454 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			454 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
import fs from "fs";
 | 
						|
import configs from "../../system/config.js";
 | 
						|
import {
 | 
						|
  getPathLocalData,
 | 
						|
  getPathProfile,
 | 
						|
  safeClosePage,
 | 
						|
} from "../../system/utils.js";
 | 
						|
import { ApiBid } from "../api-bid.js";
 | 
						|
import _ from "lodash";
 | 
						|
import { updateStatusByPrice } from "../../system/apis/bid.js";
 | 
						|
 | 
						|
export class LangtonsApiBid extends ApiBid {
 | 
						|
  reloadInterval = null;
 | 
						|
  constructor({ ...prev }) {
 | 
						|
    super(prev);
 | 
						|
  }
 | 
						|
 | 
						|
  waitVerifyData = async () =>
 | 
						|
    new Promise((rev, rej) => {
 | 
						|
      // Tạo timeout để reject sau 1 phút nếu không có phản hồi
 | 
						|
      const timeout = setTimeout(() => {
 | 
						|
        global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ
 | 
						|
        rej(
 | 
						|
          new Error(
 | 
						|
            `[${this.id}] Timeout: No verification code received within 1 minute.`
 | 
						|
          )
 | 
						|
        );
 | 
						|
      }, 120 * 1000); // 120 giây
 | 
						|
 | 
						|
      global.socket.on(`verify-code.${this.origin_url}`, async (data) => {
 | 
						|
        console.log(`📢 [${this.id}] VERIFY CODE:`, data);
 | 
						|
        clearTimeout(timeout); // Hủy timeout vì đã nhận được mã
 | 
						|
        global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại
 | 
						|
        rev(data); // Resolve với dữ liệu nhận được
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
  isLogin = async () => {
 | 
						|
    if (!this.page_context) return false;
 | 
						|
 | 
						|
    const filePath = getPathProfile(this.origin_url);
 | 
						|
 | 
						|
    return (
 | 
						|
      !(await this.page_context.$('input[name="loginEmail"]')) &&
 | 
						|
      fs.existsSync(filePath)
 | 
						|
    );
 | 
						|
  };
 | 
						|
 | 
						|
  async callSubmitPrevCodeApi(code, csrfToken) {
 | 
						|
    if (!this.page_context) return;
 | 
						|
    try {
 | 
						|
      const result = await this.page_context.evaluate(
 | 
						|
        async (code, csrfToken) => {
 | 
						|
          const formData = new FormData();
 | 
						|
          formData.append("dwfrm_profile_login_code", code);
 | 
						|
          formData.append("csrf_token", csrfToken);
 | 
						|
 | 
						|
          try {
 | 
						|
            const response = await fetch(
 | 
						|
              "https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Login-VerifyOtpForLogin",
 | 
						|
              {
 | 
						|
                method: "POST",
 | 
						|
                body: formData,
 | 
						|
              }
 | 
						|
            );
 | 
						|
 | 
						|
            const data = await response.json();
 | 
						|
            return { success: true, data };
 | 
						|
          } catch (error) {
 | 
						|
            return { success: false, error: error.toString() };
 | 
						|
          }
 | 
						|
        },
 | 
						|
        code,
 | 
						|
        csrfToken
 | 
						|
      ); // truyền biến vào page context
 | 
						|
 | 
						|
      if (result.success) {
 | 
						|
        console.log(`[${this.id}] callInsideApi API response:`, result.data);
 | 
						|
        return result.data;
 | 
						|
      } else {
 | 
						|
        console.error(`[${this.id}] callInsideApi API error:`, result.error);
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      console.error(`[${this.id}] Puppeteer evaluate error:`, error);
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async callSubmitAccountApi(csrfToken) {
 | 
						|
    if (!this.page_context) return;
 | 
						|
    try {
 | 
						|
      const result = await this.page_context.evaluate(
 | 
						|
        async ({ username, password }, csrfToken) => {
 | 
						|
          const formData = new FormData();
 | 
						|
          formData.append("loginEmail", username);
 | 
						|
          formData.append("loginPassword", password);
 | 
						|
          formData.append("rememberMe", true);
 | 
						|
          formData.append("csrf_token", csrfToken);
 | 
						|
 | 
						|
          try {
 | 
						|
            const response = await fetch(
 | 
						|
              "https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Account-Login?rurl=5",
 | 
						|
              {
 | 
						|
                method: "POST",
 | 
						|
                body: formData,
 | 
						|
              }
 | 
						|
            );
 | 
						|
 | 
						|
            const data = await response.json();
 | 
						|
            return { success: true, data };
 | 
						|
          } catch (error) {
 | 
						|
            return { success: false, error: error.toString() };
 | 
						|
          }
 | 
						|
        },
 | 
						|
        { username: this.username, password: this.password },
 | 
						|
        csrfToken
 | 
						|
      ); // truyền biến vào page context
 | 
						|
 | 
						|
      if (result.success) {
 | 
						|
        console.log(
 | 
						|
          `[${this.id}] callSubmitAccountApi API response:`,
 | 
						|
          result.data
 | 
						|
        );
 | 
						|
        return result.data;
 | 
						|
      } else {
 | 
						|
        console.error(
 | 
						|
          `[${this.id}] callSubmitAccountApi API error:`,
 | 
						|
          result.error
 | 
						|
        );
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      console.error(`[${this.id}] Puppeteer evaluate error:`, error);
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async getCsrfToken() {
 | 
						|
    try {
 | 
						|
      const csrfToken = await this.page_context.evaluate(() => {
 | 
						|
        const csrfInput = document.querySelector(
 | 
						|
          'input[name*="csrf"], input[name="_token"]'
 | 
						|
        );
 | 
						|
        return csrfInput ? csrfInput.value : null;
 | 
						|
      });
 | 
						|
 | 
						|
      if (csrfToken) {
 | 
						|
        console.log(`✅ [${this.id}] CSRF token: ${csrfToken}`);
 | 
						|
        return csrfToken;
 | 
						|
      } else {
 | 
						|
        console.warn(`⚠️ [${this.id}] No CSRF token found.`);
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
    } catch (error) {
 | 
						|
      console.error(`❌ [${this.id}] Error getting CSRF token:`, error);
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async submitCode({ name, code }) {
 | 
						|
    try {
 | 
						|
      const csrfToken = await this.getCsrfToken();
 | 
						|
 | 
						|
      if (!csrfToken) return false;
 | 
						|
 | 
						|
      const responsePrevCode = await this.callSubmitPrevCodeApi(
 | 
						|
        code,
 | 
						|
        csrfToken
 | 
						|
      );
 | 
						|
 | 
						|
      if (!responsePrevCode || !responsePrevCode.success) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
 | 
						|
      const responseAccount = await this.callSubmitAccountApi(csrfToken);
 | 
						|
 | 
						|
      if (!responseAccount || !responseAccount.success) {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
 | 
						|
      return true;
 | 
						|
    } catch (error) {
 | 
						|
      console.error(`❌ [${this.id}] Error submitPrevCode:`, error);
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async handleLogin() {
 | 
						|
    const page = this.page_context;
 | 
						|
 | 
						|
    global.IS_CLEANING = false;
 | 
						|
 | 
						|
    const filePath = getPathProfile(this.origin_url);
 | 
						|
 | 
						|
    await page.waitForNavigation({ waitUntil: "domcontentloaded" });
 | 
						|
 | 
						|
    // 🛠 Check if already logged in (login input should not be visible or profile exists)
 | 
						|
    if (
 | 
						|
      !(await page.$('input[name="loginEmail"]')) &&
 | 
						|
      fs.existsSync(filePath)
 | 
						|
    ) {
 | 
						|
      console.log(`✅ [${this.id}] Already logged in, skipping login process.`);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // // check valid prev code
 | 
						|
    // if (this.isCodeValid()) {
 | 
						|
    //   const result = await this.submitPrevCode();
 | 
						|
 | 
						|
    //   if (result) return;
 | 
						|
    // }
 | 
						|
 | 
						|
    if (fs.existsSync(filePath)) {
 | 
						|
      console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
 | 
						|
      fs.unlinkSync(filePath);
 | 
						|
    }
 | 
						|
 | 
						|
    const children = this.children.filter((item) => item.page_context);
 | 
						|
    console.log(
 | 
						|
      `🔍 [${this.id}] Found ${children.length} child pages to close.`
 | 
						|
    );
 | 
						|
 | 
						|
    if (children.length > 0) {
 | 
						|
      console.log(`🛑 [${this.id}] Closing child pages...`);
 | 
						|
      await Promise.all(
 | 
						|
        children.map((item) => {
 | 
						|
          console.log(
 | 
						|
            `➡ [${this.id}] Closing child page with context: ${item.page_context}`
 | 
						|
          );
 | 
						|
          return safeClosePage(item);
 | 
						|
        })
 | 
						|
      );
 | 
						|
 | 
						|
      console.log(
 | 
						|
        `➡ [${this.id}] Closing main page context: ${this.page_context}`
 | 
						|
      );
 | 
						|
      await safeClosePage(this);
 | 
						|
 | 
						|
      await this.onCloseLogin(this);
 | 
						|
    }
 | 
						|
 | 
						|
    console.log(`🔑 [${this.id}] Starting login process...`);
 | 
						|
 | 
						|
    try {
 | 
						|
      // ⌨ Enter email
 | 
						|
      console.log(`✍ [${this.id}] Entering email:`, this.username);
 | 
						|
      await page.type('input[name="loginEmail"]', this.username, {
 | 
						|
        delay: 100,
 | 
						|
      });
 | 
						|
 | 
						|
      // ⌨ Enter password
 | 
						|
      console.log(`✍ [${this.id}] Entering password...`);
 | 
						|
      await page.type('input[name="loginPassword"]', this.password, {
 | 
						|
        delay: 150,
 | 
						|
      });
 | 
						|
 | 
						|
      // ✅ Click the "Remember Me" checkbox
 | 
						|
      console.log(`🔘 [${this.id}] Clicking the "Remember Me" checkbox`);
 | 
						|
      await page.click("#rememberMe", { delay: 80 });
 | 
						|
 | 
						|
      // 🚀 Click the login button
 | 
						|
      console.log(`🔘 [${this.id}] Clicking the "Login" button`);
 | 
						|
      await page.click("#loginFormSubmitButton", { delay: 92 });
 | 
						|
 | 
						|
      // ⏳ Wait for navigation after login
 | 
						|
      console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
 | 
						|
      await page.waitForNavigation({
 | 
						|
        timeout: 8000,
 | 
						|
        waitUntil: "domcontentloaded",
 | 
						|
      });
 | 
						|
 | 
						|
      console.log(`🌍 [${this.id}] Current page after login:`, page.url());
 | 
						|
 | 
						|
      // 📢 Listen for verification code event
 | 
						|
      console.log(
 | 
						|
        `👂 [${this.id}] Listening for event: verify-code.${this.origin_url}`
 | 
						|
      );
 | 
						|
 | 
						|
      // ⏳ Wait for verification code from socket event
 | 
						|
      const { name, code } = await this.waitVerifyData();
 | 
						|
      console.log(`✅ [${this.id}] Verification code received:`, {
 | 
						|
        name,
 | 
						|
        code,
 | 
						|
      });
 | 
						|
 | 
						|
      // save code to local
 | 
						|
      // await this.saveCodeToLocal({ name, code });
 | 
						|
 | 
						|
      // // ⌨ Enter verification code
 | 
						|
      // console.log(`✍ [${this.id}] Entering verification code...`);
 | 
						|
      // await page.type("#code", code, { delay: 120 });
 | 
						|
 | 
						|
      // // 🚀 Click the verification confirmation button
 | 
						|
      // console.log(
 | 
						|
      //   `🔘 [${this.id}] Clicking the verification confirmation button`
 | 
						|
      // );
 | 
						|
      // await page.click(".btn.btn-block.btn-primary", { delay: 90 });
 | 
						|
 | 
						|
      const reuslt = await this.submitCode({ name, code });
 | 
						|
 | 
						|
      if (!reuslt) {
 | 
						|
        console.log(`[${this.id}] Wrote verifi code failure`);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // // ⏳ Wait for navigation after verification
 | 
						|
      // console.log(
 | 
						|
      //   `⏳ [${this.id}] Waiting for navigation after verification...`
 | 
						|
      // );
 | 
						|
      // await page.waitForNavigation({
 | 
						|
      //   timeout: 15000,
 | 
						|
      //   waitUntil: "domcontentloaded",
 | 
						|
      // });
 | 
						|
 | 
						|
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
						|
 | 
						|
      // 📂 Save session context to avoid re-login
 | 
						|
      await this.saveContext();
 | 
						|
      console.log(`✅ [${this.id}] Login successful!`);
 | 
						|
 | 
						|
      // await page.goto(this.url);
 | 
						|
      console.log(`✅ [${this.id}] Navigation successful!`);
 | 
						|
 | 
						|
      // clear code
 | 
						|
      // this.clearCodeFromLocal();
 | 
						|
    } catch (error) {
 | 
						|
      console.error(
 | 
						|
        `❌ [${this.id}] Error during login process:`,
 | 
						|
        error.message
 | 
						|
      );
 | 
						|
    } finally {
 | 
						|
      global.IS_CLEANING = true;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async getWonList() {
 | 
						|
    try {
 | 
						|
      await page.waitForSelector(".row.account-product-list", {
 | 
						|
        timeout: 30000,
 | 
						|
      });
 | 
						|
 | 
						|
      const items = await page.evaluate(() => {
 | 
						|
        return Array.from(
 | 
						|
          document.querySelectorAll(".row.account-product-list")
 | 
						|
        ).map((item) => item.getAttribute("data-lotid") || null);
 | 
						|
      });
 | 
						|
 | 
						|
      return _.compact(items);
 | 
						|
    } catch (error) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async handleUpdateWonItem() {
 | 
						|
    console.log(`🔄 [${this.id}] Starting to update the won list...`);
 | 
						|
 | 
						|
    // Lấy danh sách các lot_id thắng
 | 
						|
    const items = await this.getWonList();
 | 
						|
    console.log(`📌 [${this.id}] List of won lot_ids:`, items);
 | 
						|
 | 
						|
    // Nếu không có item nào, thoát ra
 | 
						|
    if (items.length === 0) {
 | 
						|
      console.log(`⚠️ [${this.id}] No items to update.`);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
 | 
						|
    const result = _.filter(this.children, (item) =>
 | 
						|
      _.includes(items, item.lot_id)
 | 
						|
    );
 | 
						|
    console.log(
 | 
						|
      `✅ [${this.id}] ${result.length} items need to be updated:`,
 | 
						|
      result
 | 
						|
    );
 | 
						|
 | 
						|
    // Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
 | 
						|
    const responses = await Promise.allSettled(
 | 
						|
      result.map((i) => updateStatusByPrice(i.id, i.current_price))
 | 
						|
    );
 | 
						|
 | 
						|
    // Log kết quả của mỗi request
 | 
						|
    responses.forEach((response, index) => {
 | 
						|
      if (response.status === "fulfilled") {
 | 
						|
        console.log(`✔️ [${this.id}] Successfully updated:`, result[index]);
 | 
						|
      } else {
 | 
						|
        console.error(
 | 
						|
          `❌ [${this.id}] Update failed:`,
 | 
						|
          result[index],
 | 
						|
          response.reason
 | 
						|
        );
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    console.log(`🏁 [${this.id}] Finished updating the won list.`);
 | 
						|
    return responses;
 | 
						|
  }
 | 
						|
 | 
						|
  action = async () => {
 | 
						|
    try {
 | 
						|
      const page = this.page_context;
 | 
						|
 | 
						|
      page.on("response", async (response) => {
 | 
						|
        const request = response.request();
 | 
						|
        if (request.redirectChain().length > 0) {
 | 
						|
          if (response.url().includes(configs.WEB_CONFIGS.LANGTONS.LOGIN_URL)) {
 | 
						|
            await this.handleLogin();
 | 
						|
          }
 | 
						|
        }
 | 
						|
      });
 | 
						|
 | 
						|
      await page.goto(this.url, { waitUntil: "networkidle2" });
 | 
						|
 | 
						|
      await page.bringToFront();
 | 
						|
 | 
						|
      // Set userAgent
 | 
						|
      await page.setUserAgent(
 | 
						|
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
 | 
						|
      );
 | 
						|
    } catch (error) {
 | 
						|
      console.log("Error [action]: ", error.message);
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  listen_events = async () => {
 | 
						|
    if (this.page_context) return;
 | 
						|
 | 
						|
    // await this.puppeteer_connect();
 | 
						|
    // await this.action();
 | 
						|
    const results = await this.handlePrevListen();
 | 
						|
 | 
						|
    if (!results) return;
 | 
						|
 | 
						|
    this.reloadInterval = setInterval(async () => {
 | 
						|
      try {
 | 
						|
        if (this.page_context && !this.page_context.isClosed()) {
 | 
						|
          console.log(`🔄 [${this.id}] Reloading page...`);
 | 
						|
          await this.page_context.reload({ waitUntil: "networkidle2" });
 | 
						|
          console.log(`✅ [${this.id}] Page reloaded successfully.`);
 | 
						|
 | 
						|
          // this.handleUpdateWonItem();
 | 
						|
        } else {
 | 
						|
          console.log(
 | 
						|
            `❌ [${this.id}] Page context is closed. Stopping reload.`
 | 
						|
          );
 | 
						|
          clearInterval(this.reloadInterval);
 | 
						|
        }
 | 
						|
      } catch (error) {
 | 
						|
        console.error(`🚨 [${this.id}] Error reloading page:`, error.message);
 | 
						|
      }
 | 
						|
    }, 60000); // 1p reload
 | 
						|
  };
 | 
						|
}
 |