521 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			521 lines
		
	
	
		
			15 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();
 | 
						|
 | 
						|
let timeToUpdateLogin = new Date().toUTCString();
 | 
						|
 | 
						|
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) {
 | 
						|
            if (global[`IS_PLACE_BID-${productTab.id}`]) return;
 | 
						|
 | 
						|
            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 {
 | 
						|
          if (!isTimeReached(timeToUpdateLogin)) return;
 | 
						|
 | 
						|
          const login_status = await item.isLogin();
 | 
						|
 | 
						|
          await updateLoginStatus({
 | 
						|
            data: {
 | 
						|
              id: item.id,
 | 
						|
              type: item.type,
 | 
						|
              origin_url: item.origin_url,
 | 
						|
            },
 | 
						|
            login_status,
 | 
						|
          });
 | 
						|
 | 
						|
          // Set time to update login sau 1 phút
 | 
						|
          const now = new Date();
 | 
						|
          const oneMinuteLater = new Date(now.getTime() + 60 * 1000);
 | 
						|
 | 
						|
          timeToUpdateLogin = oneMinuteLater;
 | 
						|
        } 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();
 | 
						|
})();
 |