509 lines
14 KiB
JavaScript
509 lines
14 KiB
JavaScript
import "dotenv/config";
|
|
import _ from "lodash";
|
|
import pLimit from "p-limit";
|
|
import { io } from "socket.io-client";
|
|
import {
|
|
createApiBid,
|
|
createBidProduct,
|
|
deleteProfile,
|
|
shouldUpdateProductTab,
|
|
} from "./service/app-service.js";
|
|
import { updateLoginStatus } from "./system/apis/bid.js";
|
|
import browser from "./system/browser.js";
|
|
import configs from "./system/config.js";
|
|
import {
|
|
delay,
|
|
findNearestClosingChild,
|
|
isTimeReached,
|
|
safeClosePage,
|
|
subtractSeconds,
|
|
} from "./system/utils.js";
|
|
|
|
global.IS_CLEANING = true;
|
|
|
|
let MANAGER_BIDS = [];
|
|
|
|
const activeTasks = new Set();
|
|
|
|
const handleUpdateProductTabs = (data) => {
|
|
if (!Array.isArray(data)) {
|
|
console.log("Data must be array");
|
|
return;
|
|
}
|
|
|
|
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
|
|
|
|
const newDataManager = data.map(({ children, ...web }) => {
|
|
const prevApiBid = managerBidMap.get(web.id);
|
|
|
|
const newChildren = children.map((item) => {
|
|
const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id);
|
|
|
|
if (prevProductTab) {
|
|
prevProductTab.setNewData(item);
|
|
|
|
return prevProductTab;
|
|
}
|
|
|
|
return createBidProduct(web, item);
|
|
});
|
|
|
|
if (prevApiBid) {
|
|
prevApiBid.setNewData({ children: newChildren, ...web });
|
|
return prevApiBid;
|
|
}
|
|
|
|
return createApiBid({ ...web, children: newChildren });
|
|
});
|
|
|
|
MANAGER_BIDS = newDataManager;
|
|
};
|
|
|
|
const addProductTab = (data) => {
|
|
if (
|
|
typeof data !== "object" ||
|
|
data === null ||
|
|
!Array.isArray(data.children)
|
|
) {
|
|
console.warn("Data must be an object with a children array");
|
|
return;
|
|
}
|
|
|
|
const { children, ...web } = data;
|
|
|
|
if (children.length === 0) {
|
|
console.warn(
|
|
`⚠️ No children found for bid id ${web.id}, skipping addProductTab`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
|
|
const prevApiBid = managerBidMap.get(web.id);
|
|
|
|
if (prevApiBid) {
|
|
// Cập nhật
|
|
const updatedChildren = prevApiBid.children;
|
|
|
|
children.forEach((newChild) => {
|
|
const existingChildIndex = updatedChildren.findIndex(
|
|
(c) => c.id === newChild.id
|
|
);
|
|
|
|
if (existingChildIndex !== -1) {
|
|
updatedChildren[existingChildIndex].setNewData(newChild);
|
|
} else {
|
|
updatedChildren.push(createBidProduct(web, newChild));
|
|
}
|
|
});
|
|
|
|
prevApiBid.setNewData({ ...web, children: updatedChildren });
|
|
} else {
|
|
// Tạo mới
|
|
const newChildren = children.map((item) => createBidProduct(web, item));
|
|
const newApiBid = createApiBid({ ...web, children: newChildren });
|
|
|
|
MANAGER_BIDS.push(newApiBid);
|
|
|
|
console.log("%cindex.js:116 {MANAGER_BIDS}", "color: #007acc;", {
|
|
MANAGER_BIDS,
|
|
children: newChildren,
|
|
});
|
|
}
|
|
};
|
|
|
|
const tracking = async () => {
|
|
console.log("🚀 Tracking process started...");
|
|
|
|
while (true) {
|
|
try {
|
|
console.log("🔍 Scanning active bids...");
|
|
const productTabs = _.flatMap(MANAGER_BIDS, "children");
|
|
|
|
await Promise.allSettled(
|
|
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
|
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
|
|
return apiBid.listen_events();
|
|
})
|
|
);
|
|
|
|
await Promise.allSettled(
|
|
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
|
|
console.log(`🎧 Listening to events for close login: ${apiBid.id}`);
|
|
return (apiBid.onCloseLogin = (data) => {
|
|
// Loại bỏ class hiện có. Tạo tiền đề cho việc tạo đối tượng mới lại
|
|
MANAGER_BIDS = MANAGER_BIDS.filter(
|
|
(item) => item.id !== data.id && item.type !== data.type
|
|
);
|
|
|
|
addProductTab(data);
|
|
});
|
|
})
|
|
);
|
|
|
|
Promise.allSettled(
|
|
productTabs.map(async (productTab) => {
|
|
console.log(`📌 Processing Product ID: ${productTab.id}`);
|
|
|
|
// Xác định parent context
|
|
if (!productTab.parent_browser_context) {
|
|
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
|
|
productTab.parent_browser_context = parent?.browser_context;
|
|
if (!productTab.parent_browser_context) {
|
|
console.log(
|
|
`⏳ Waiting for parent process... (Product ID: ${productTab.id})`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Thời điểm tracking liên tục
|
|
const earlyTrackingTime = subtractSeconds(
|
|
productTab.close_time,
|
|
productTab?.web_bid?.early_tracking_seconds || 0
|
|
);
|
|
|
|
// Check không mở tab nếu chưa đến giờ
|
|
if (productTab.close_time && !isTimeReached(earlyTrackingTime)) {
|
|
console.log(
|
|
`⏳ [${productTab.id}] Early tracking time not reached yet. ` +
|
|
`Waiting until ${earlyTrackingTime} (current time: ${new Date().toISOString()})`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Kết nối Puppeteer nếu chưa có page_context
|
|
if (!productTab.page_context) {
|
|
console.log(
|
|
`🔌 Connecting to page for Product ID: ${productTab.id}`
|
|
);
|
|
await productTab.puppeteer_connect();
|
|
}
|
|
|
|
// Kiểm tra URL và điều hướng nếu cần
|
|
if ((await productTab.page_context.url()) !== productTab.url) {
|
|
console.log(
|
|
`🔄 Redirecting to new URL for Product ID: ${productTab.id}`
|
|
);
|
|
await productTab.gotoLink();
|
|
}
|
|
|
|
// Cập nhật nếu cần thiết
|
|
if (shouldUpdateProductTab(productTab)) {
|
|
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
|
|
await productTab.update();
|
|
} else {
|
|
console.log(
|
|
`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`
|
|
);
|
|
}
|
|
|
|
// Chờ first bid
|
|
if (!productTab.first_bid) {
|
|
console.log(
|
|
`🎯 Waiting for first bid for Product ID: ${productTab.id}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Kiểm tra thời gian bid
|
|
if (
|
|
productTab.start_bid_time &&
|
|
!isTimeReached(productTab.start_bid_time)
|
|
) {
|
|
console.log(
|
|
`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Thực thi hành động
|
|
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
|
|
await productTab.action();
|
|
})
|
|
);
|
|
|
|
// Dọn dẹp tab không dùng
|
|
console.log("🧹 Cleaning up unused tabs...");
|
|
clearLazyTab();
|
|
|
|
// Cập nhật trạng thái tracking
|
|
console.log("📊 Tracking work status...");
|
|
workTracking();
|
|
|
|
// Bắn event status login
|
|
console.log("📊 Tracking login status...");
|
|
trackingLoginStatus();
|
|
} catch (error) {
|
|
console.error("❌ Error in tracking loop:", error);
|
|
}
|
|
|
|
console.log(
|
|
`⏳ Waiting ${
|
|
configs.AUTO_TRACKING_DELAY / 1000
|
|
} seconds before the next iteration...`
|
|
);
|
|
await delay(configs.AUTO_TRACKING_DELAY);
|
|
}
|
|
};
|
|
|
|
const clearLazyTab = async () => {
|
|
if (!global.IS_CLEANING) {
|
|
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
|
|
return;
|
|
}
|
|
|
|
if (!browser) {
|
|
console.warn("⚠️ Browser is not available or disconnected.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const pages = await browser.pages();
|
|
console.log("🔍 Found pages:", pages.length);
|
|
|
|
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
|
|
item.url,
|
|
...item.children.map((child) => child.url),
|
|
]).filter(Boolean);
|
|
|
|
// product tabs
|
|
const productTabs = _.flatMap(MANAGER_BIDS, "children");
|
|
|
|
// for (const item of [...productTabs, ...MANAGER_BIDS]) {
|
|
// if (!item.page_context) continue;
|
|
|
|
// try {
|
|
// const avalableResult = await isPageAvailable(item.page_context);
|
|
|
|
// if (!avalableResult) {
|
|
// await safeClosePage(item);
|
|
// }
|
|
// } catch (e) {
|
|
// console.warn("⚠️ Error checking page_context.title()", e.message);
|
|
// await safeClosePage(item);
|
|
// }
|
|
// }
|
|
|
|
for (const page of pages) {
|
|
try {
|
|
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
|
|
|
|
const pageUrl = page.url();
|
|
|
|
if (!pageUrl || pageUrl === "about:blank") continue;
|
|
|
|
if (activeUrls.includes(pageUrl)) {
|
|
const productTab = productTabs.find((item) => item.url === pageUrl);
|
|
|
|
if (!productTab || !productTab?.close_time) continue;
|
|
|
|
const earlyTrackingTime = subtractSeconds(
|
|
productTab.close_time,
|
|
productTab?.web_bid?.early_tracking_seconds || 0
|
|
);
|
|
|
|
if (!isTimeReached(earlyTrackingTime)) {
|
|
await safeClosePage(productTab);
|
|
console.log(`🛑 Unused page detected: ${pageUrl}`);
|
|
|
|
continue;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// remove all listents
|
|
page.removeAllListeners();
|
|
|
|
console.log(`🛑 Unused page detected: ${pageUrl}`);
|
|
|
|
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
|
|
.map((i) => ({
|
|
current_url: i.page_context.url(),
|
|
data: i,
|
|
}))
|
|
.find((j) => j.current_url === pageUrl);
|
|
|
|
if (bidData && bidData.data) {
|
|
await safeClosePage(bidData.data);
|
|
} else {
|
|
try {
|
|
await Promise.race([
|
|
page.close(),
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => reject(new Error("Close timeout")), 3000)
|
|
),
|
|
]);
|
|
} catch (closeErr) {
|
|
console.warn(
|
|
`⚠️ Error closing page ${pageUrl}: ${closeErr.message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(`✅ Closed page: ${pageUrl}`);
|
|
} catch (pageErr) {
|
|
console.warn(`⚠️ Error handling page: ${pageErr.message}`);
|
|
}
|
|
}
|
|
|
|
// Delete lazy tracking page
|
|
Promise.allSettled(
|
|
MANAGER_BIDS.map(async (item) => {
|
|
if (await item.isLazy()) {
|
|
safeClosePage(item);
|
|
}
|
|
})
|
|
);
|
|
} catch (err) {
|
|
console.error("❌ Error in clearLazyTab:", err.message);
|
|
}
|
|
};
|
|
|
|
const workTracking = async () => {
|
|
try {
|
|
const activeData = _.flatMap(MANAGER_BIDS, (item) => [
|
|
item,
|
|
...item.children,
|
|
]);
|
|
const limit = pLimit(5);
|
|
|
|
await Promise.allSettled(
|
|
activeData
|
|
.filter((item) => item.page_context && !item.page_context.isClosed())
|
|
.filter((item) => !activeTasks.has(item.id))
|
|
.map((item) =>
|
|
limit(async () => {
|
|
activeTasks.add(item.id);
|
|
try {
|
|
await item.handleTakeWorkSnapshot();
|
|
} catch (error) {
|
|
console.error(
|
|
`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`,
|
|
error
|
|
);
|
|
} finally {
|
|
activeTasks.delete(item.id);
|
|
}
|
|
})
|
|
)
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
`[❌ ERROR] Work tracking failed: ${error.message}\n`,
|
|
error.stack
|
|
);
|
|
}
|
|
};
|
|
|
|
const trackingLoginStatus = async () => {
|
|
try {
|
|
if (!MANAGER_BIDS?.length) return;
|
|
|
|
const results = await Promise.allSettled(
|
|
MANAGER_BIDS.map(async (item) => {
|
|
try {
|
|
const login_status = await item.isLogin();
|
|
|
|
await updateLoginStatus({
|
|
data: {
|
|
id: item.id,
|
|
type: item.type,
|
|
origin_url: item.origin_url,
|
|
},
|
|
login_status,
|
|
});
|
|
} catch (err) {
|
|
console.warn(
|
|
`[⚠️ WARN] Failed to check login for bid ${
|
|
item?.id || "unknown"
|
|
}: ${err.message}`
|
|
);
|
|
}
|
|
})
|
|
);
|
|
|
|
// Optional: log summary
|
|
const failed = results.filter((r) => r.status === "rejected").length;
|
|
if (failed) {
|
|
console.warn(`[⚠️ WARN] ${failed} login status checks failed.`);
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
`[❌ ERROR] Login status tracking failed: ${error.message}\n`,
|
|
error.stack
|
|
);
|
|
}
|
|
};
|
|
|
|
(async () => {
|
|
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
|
|
transports: ["websocket"],
|
|
reconnection: true,
|
|
extraHeaders: {
|
|
Authorization: process.env.CLIENT_KEY,
|
|
},
|
|
});
|
|
|
|
// set socket on global app
|
|
global.socket = socket;
|
|
|
|
// listen connect
|
|
socket.on("connect", () => {
|
|
console.log("✅ Connected to WebSocket server");
|
|
console.log("🔗 Socket ID:", socket.id);
|
|
});
|
|
|
|
// listen connect
|
|
socket.on("disconnect", () => {
|
|
console.log("❌Client key is valid. Disconnected");
|
|
});
|
|
|
|
// listen event
|
|
socket.on("bidsUpdated", async (data) => {
|
|
console.log("📢 Bids Data:", data);
|
|
|
|
handleUpdateProductTabs(data);
|
|
});
|
|
|
|
socket.on("webUpdated", async (data) => {
|
|
console.log("📢 Account was updated:", data);
|
|
|
|
const webBid = MANAGER_BIDS.find((item) => item?.id === data?.id);
|
|
|
|
if (
|
|
webBid &&
|
|
webBid.username === data.username &&
|
|
webBid.password === data.password
|
|
)
|
|
return;
|
|
|
|
const isDeleted = deleteProfile(data);
|
|
|
|
if (isDeleted) {
|
|
console.log("✅ Profile deleted successfully!");
|
|
|
|
const tab = MANAGER_BIDS.find((item) => item.url === data.url);
|
|
|
|
if (!tab) return;
|
|
|
|
global.IS_CLEANING = false;
|
|
await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
|
|
|
|
await safeClosePage(tab);
|
|
|
|
MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id);
|
|
|
|
addProductTab(data);
|
|
|
|
global.IS_CLEANING = true;
|
|
} else {
|
|
console.log("⚠️ No profile found to delete.");
|
|
}
|
|
});
|
|
|
|
// AUTO TRACKING
|
|
tracking();
|
|
})();
|