325 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			325 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
import CONSTANTS from "./constants.js";
 | 
						|
import fs from "fs";
 | 
						|
import path from "path";
 | 
						|
import { updateStatusWork } from "./apis/bid.js";
 | 
						|
import _ from "lodash";
 | 
						|
 | 
						|
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
 | 
						|
 | 
						|
export const takeSnapshot = async (
 | 
						|
  page,
 | 
						|
  item,
 | 
						|
  imageName,
 | 
						|
  type = CONSTANTS.TYPE_IMAGE.ERRORS
 | 
						|
) => {
 | 
						|
  if (!page || page.isClosed()) return;
 | 
						|
 | 
						|
  try {
 | 
						|
    const baseDir = path.join(
 | 
						|
      CONSTANTS.ERROR_IMAGES_PATH,
 | 
						|
      item.type,
 | 
						|
      String(item.id)
 | 
						|
    ); // Thư mục theo lot_id
 | 
						|
    const typeDir = path.join(baseDir, type); // Thư mục con theo type
 | 
						|
 | 
						|
    // Tạo tên file, nếu type === 'work' thì không có timestamp
 | 
						|
    const fileName =
 | 
						|
      type === CONSTANTS.TYPE_IMAGE.WORK
 | 
						|
        ? `${imageName}.png`
 | 
						|
        : `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
 | 
						|
 | 
						|
    const filePath = path.join(typeDir, fileName);
 | 
						|
 | 
						|
    // Kiểm tra và tạo thư mục nếu chưa tồn tại
 | 
						|
    if (!fs.existsSync(typeDir)) {
 | 
						|
      fs.mkdirSync(typeDir, { recursive: true });
 | 
						|
      console.log(`📂 Save at folder: ${typeDir}`);
 | 
						|
    }
 | 
						|
 | 
						|
    // await page.waitForSelector('body', { visible: true, timeout: 5000 });
 | 
						|
    // Kiểm tra có thể điều hướng trang không
 | 
						|
    const isPageResponsive = await page.evaluate(
 | 
						|
      () => document.readyState === "complete"
 | 
						|
    );
 | 
						|
    if (!isPageResponsive) {
 | 
						|
      console.log("🚫 Page is unresponsive, skipping snapshot.");
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
 | 
						|
    await page
 | 
						|
      .waitForSelector("body", { visible: true, timeout: 15000 })
 | 
						|
      .catch(() => {
 | 
						|
        console.log("⚠️ Body selector not found, skipping snapshot.");
 | 
						|
        return;
 | 
						|
      });
 | 
						|
 | 
						|
    // Chụp ảnh màn hình và lưu vào filePath
 | 
						|
    await page.screenshot({ path: filePath });
 | 
						|
 | 
						|
    console.log(`📸 Image saved at: ${filePath}`);
 | 
						|
 | 
						|
    // Nếu type === 'work', gửi ảnh lên API
 | 
						|
    if (type === CONSTANTS.TYPE_IMAGE.WORK) {
 | 
						|
      await updateStatusWork(item, filePath);
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    console.log("Error when snapshot: " + error.message);
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 | 
						|
 | 
						|
export const safeClosePageReal = async (page) => {
 | 
						|
  if (!page) return;
 | 
						|
 | 
						|
  try {
 | 
						|
    if (page.isClosed()) {
 | 
						|
      console.log(`✅ Page already closed: ${page.url()}`);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    page.removeAllListeners(); // ✂️ Remove hết listeners trước khi close
 | 
						|
    await page.close({ runBeforeUnload: true }); // 🛑 Đóng an toàn
 | 
						|
    console.log(`✅ Successfully closed page: ${page.url()}`);
 | 
						|
  } catch (err) {
 | 
						|
    console.warn(
 | 
						|
      `⚠️ Error closing page ${page.url ? page.url() : ""}: ${err.message}`
 | 
						|
    );
 | 
						|
  }
 | 
						|
};
 | 
						|
 | 
						|
export async function safeClosePage(item) {
 | 
						|
  try {
 | 
						|
    const page = item.page_context;
 | 
						|
 | 
						|
    if (page && !page.isClosed() && page.close) {
 | 
						|
      await safeClosePageReal(page);
 | 
						|
    }
 | 
						|
 | 
						|
    if (item?.page_context) {
 | 
						|
      item.page_context = undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    if (item?.browser_context) {
 | 
						|
      try {
 | 
						|
        await item.browser_context.close();
 | 
						|
      } catch (ctxErr) {
 | 
						|
        console.warn(
 | 
						|
          `⚠️ Failed to close browser context for item ${item.id}: ${ctxErr.message}`
 | 
						|
        );
 | 
						|
      }
 | 
						|
      item.browser_context = undefined;
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    console.warn(`⚠️ Can't close item ${item?.id}: ${error.message}`);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function isTimeReached(targetTime) {
 | 
						|
  if (!targetTime) return false;
 | 
						|
 | 
						|
  const targetDate = new Date(targetTime);
 | 
						|
  const now = new Date();
 | 
						|
 | 
						|
  return now >= targetDate;
 | 
						|
}
 | 
						|
 | 
						|
export function extractNumber(str) {
 | 
						|
  const match = str.match(/\d+(\.\d+)?/);
 | 
						|
  return match ? parseFloat(match[0]) : null;
 | 
						|
}
 | 
						|
 | 
						|
export const sanitizeFileName = (url) => {
 | 
						|
  return url.replace(/[:\/]/g, "_");
 | 
						|
};
 | 
						|
 | 
						|
export const getPathProfile = (origin_url) => {
 | 
						|
  return path.join(
 | 
						|
    CONSTANTS.PROFILE_PATH,
 | 
						|
    sanitizeFileName(origin_url) + ".json"
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const getPathLocalData = (origin_url) => {
 | 
						|
  return path.join(
 | 
						|
    CONSTANTS.LOCAL_DATA_PATH,
 | 
						|
    sanitizeFileName(origin_url) + ".json"
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export function removeFalsyValues(obj, excludeKeys = []) {
 | 
						|
  return Object.entries(obj).reduce((acc, [key, value]) => {
 | 
						|
    if (value || excludeKeys.includes(key)) {
 | 
						|
      acc[key] = value;
 | 
						|
    }
 | 
						|
    return acc;
 | 
						|
  }, {});
 | 
						|
}
 | 
						|
 | 
						|
export const enableAutoBidMessage = (data) => {
 | 
						|
  return `
 | 
						|
        <b>⭉ Activate Auto Bid</b><br>
 | 
						|
    📌 Product: <b>${data.name}</b><br>
 | 
						|
    🔗 Link: <a href="${data.url}">Click here</a><br>
 | 
						|
    💰 Max Price: <b>$${data.max_price}</b><br>
 | 
						|
    🌐 Platform: <a href="${data.web_bid.origin_url}">Langtons</a>
 | 
						|
    `;
 | 
						|
};
 | 
						|
 | 
						|
export function convertAETtoUTC(dateString) {
 | 
						|
  // Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
 | 
						|
  const monthMap = {
 | 
						|
    Jan: 0,
 | 
						|
    Feb: 1,
 | 
						|
    Mar: 2,
 | 
						|
    Apr: 3,
 | 
						|
    May: 4,
 | 
						|
    Jun: 5,
 | 
						|
    Jul: 6,
 | 
						|
    Aug: 7,
 | 
						|
    Sep: 8,
 | 
						|
    Oct: 9,
 | 
						|
    Nov: 10,
 | 
						|
    Dec: 11,
 | 
						|
  };
 | 
						|
 | 
						|
  // Tách chuỗi đầu vào
 | 
						|
  const parts = dateString.match(
 | 
						|
    /(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/
 | 
						|
  );
 | 
						|
  if (!parts) {
 | 
						|
    throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
 | 
						|
  }
 | 
						|
 | 
						|
  const [, , day, month, year, hour, period] = parts;
 | 
						|
 | 
						|
  // Chuyển đổi giờ sang định dạng 24h
 | 
						|
  let hours = parseInt(hour, 10);
 | 
						|
  if (period === "PM" && hours !== 12) hours += 12;
 | 
						|
  if (period === "AM" && hours === 12) hours = 0;
 | 
						|
 | 
						|
  // Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
 | 
						|
  const date = new Date(
 | 
						|
    Date.UTC(
 | 
						|
      parseInt(year, 10),
 | 
						|
      monthMap[month],
 | 
						|
      parseInt(day, 10),
 | 
						|
      hours,
 | 
						|
      0,
 | 
						|
      0
 | 
						|
    )
 | 
						|
  );
 | 
						|
 | 
						|
  // Hàm kiểm tra DST cho AET
 | 
						|
  function isDST(date) {
 | 
						|
    const year = date.getUTCFullYear();
 | 
						|
    const month = date.getUTCMonth();
 | 
						|
    const day = date.getUTCDate();
 | 
						|
 | 
						|
    // DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT)
 | 
						|
    const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10
 | 
						|
    dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên
 | 
						|
    const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10
 | 
						|
 | 
						|
    // DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST)
 | 
						|
    const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4
 | 
						|
    dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên
 | 
						|
    const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11
 | 
						|
 | 
						|
    const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
 | 
						|
    return currentTime >= dstStartTime && currentTime < dstEndTime;
 | 
						|
  }
 | 
						|
 | 
						|
  // Xác định offset dựa trên DST
 | 
						|
  const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không
 | 
						|
 | 
						|
  // Điều chỉnh thời gian về UTC
 | 
						|
  const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
 | 
						|
 | 
						|
  // Trả về chuỗi UTC
 | 
						|
  return utcDate.toUTCString();
 | 
						|
}
 | 
						|
 | 
						|
export function extractPriceNumber(priceString) {
 | 
						|
  const cleaned = priceString.replace(/[^\d.]/g, "");
 | 
						|
  return parseFloat(cleaned);
 | 
						|
}
 | 
						|
 | 
						|
export function findEarlyLoginTime(webBid) {
 | 
						|
  const now = new Date();
 | 
						|
 | 
						|
  // Bước 1: Lọc ra những bid có close_time hợp lệ
 | 
						|
  const validChildren = webBid.children.filter((child) => child.close_time);
 | 
						|
 | 
						|
  if (validChildren.length === 0) return null;
 | 
						|
 | 
						|
  // Bước 2: Tìm bid có close_time gần hiện tại nhất
 | 
						|
  const closestBid = validChildren.reduce((closest, current) => {
 | 
						|
    const closestDiff = Math.abs(
 | 
						|
      new Date(closest.close_time).getTime() - now.getTime()
 | 
						|
    );
 | 
						|
    const currentDiff = Math.abs(
 | 
						|
      new Date(current.close_time).getTime() - now.getTime()
 | 
						|
    );
 | 
						|
    return currentDiff < closestDiff ? current : closest;
 | 
						|
  });
 | 
						|
 | 
						|
  if (!closestBid.close_time) return null;
 | 
						|
 | 
						|
  // Bước 3: Tính toán thời gian login sớm
 | 
						|
  const closeTime = new Date(closestBid.close_time);
 | 
						|
  closeTime.setSeconds(
 | 
						|
    closeTime.getSeconds() - (webBid.early_login_seconds || 0)
 | 
						|
  );
 | 
						|
 | 
						|
  return closeTime.toISOString();
 | 
						|
}
 | 
						|
 | 
						|
export function subtractMinutes(time, minutes) {
 | 
						|
  const date = new Date(time);
 | 
						|
  date.setMinutes(date.getMinutes() - minutes);
 | 
						|
  return date.toUTCString();
 | 
						|
}
 | 
						|
 | 
						|
export function subtractSeconds(time, seconds) {
 | 
						|
  const date = new Date(time);
 | 
						|
  date.setSeconds(date.getSeconds() - seconds);
 | 
						|
  return date.toUTCString();
 | 
						|
}
 | 
						|
 | 
						|
export async function isPageAvailable(page) {
 | 
						|
  if (!page || page.isClosed()) return false;
 | 
						|
 | 
						|
  try {
 | 
						|
    await Promise.race([
 | 
						|
      page.title(), // hoặc page.url(), evaluate, vv
 | 
						|
      new Promise((_, reject) =>
 | 
						|
        setTimeout(() => reject(new Error("Timeout")), 1000)
 | 
						|
      ),
 | 
						|
    ]);
 | 
						|
    return true;
 | 
						|
  } catch (err) {
 | 
						|
    console.warn(`⚠️ Page not available: ${err.message}`);
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function findNearestClosingChild(webBid) {
 | 
						|
  const now = Date.now();
 | 
						|
 | 
						|
  const validChildren = webBid.children.filter(
 | 
						|
    (child) => child.close_time && !isNaN(new Date(child.close_time).getTime())
 | 
						|
  );
 | 
						|
 | 
						|
  if (validChildren.length === 0) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  const nearestChild = _.minBy(validChildren, (child) => {
 | 
						|
    return Math.abs(new Date(child.close_time).getTime() - now);
 | 
						|
  });
 | 
						|
 | 
						|
  return nearestChild || null;
 | 
						|
}
 |