import _ from "lodash"; import { outBid, pushPrice, updateBid } from "../../system/apis/bid.js"; import { sendMessage } from "../../system/apis/notification.js"; import { createOutBidLog } from "../../system/apis/out-bid-log.js"; import configs from "../../system/config.js"; import CONSTANTS from "../../system/constants.js"; import { convertAETtoUTC, isTimeReached, randomDelayWithMeta, removeFalsyValues, takeSnapshot, } from "../../system/utils.js"; import { ProductBid } from "../product-bid.js"; export class LangtonsProductBid extends ProductBid { constructor({ ...prev }) { super(prev); } // Hàm lấy thời gian kết thúc từ trang web async getCloseTime() { try { // Kiểm tra xem có context của trang web không, nếu không thì trả về null if (!this.page_context) return null; await this.page_context.waitForSelector(".site-timezone", { timeout: 2000, }); const time = await this.page_context.evaluate(() => { const el = document.querySelector(".site-timezone"); return el ? el.innerText : null; }); return time ? convertAETtoUTC(time) : null; // return new Date(Date.now() + 6 * 60 * 1000).toUTCString(); } catch (error) { // Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null return null; } } async waitForApiResponse(timeout = 15000) { if (!this.page_context) { console.error(`❌ [${this.id}] Error: page_context is undefined.`); return null; } return new Promise((resolve) => { const onResponse = async (response) => { try { if ( !response || !response .request() .url() .includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING) ) { return; } clearTimeout(timer); // Hủy timeout nếu có phản hồi this.page_context.off("response", onResponse); // Gỡ bỏ listener const data = await response.json(); resolve(data); } catch (error) { console.error( `❌ [${this.id}] Error while parsing response:`, error?.message ); resolve(null); } }; const timer = setTimeout(async () => { console.log( `⏳ [${this.id}] Timeout: No response received within ${ timeout / 1000 }s` ); this.page_context?.off("response", onResponse); // Gỡ bỏ listener khi timeout try { if (!this.page_context.isClosed()) { await this.page_context.reload({ waitUntil: "networkidle0" }); console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`); } else { console.log(`⚠️ [${this.id}] Cannot reload, page already closed.`); } } catch (error) { console.error( `❌ [${this.id}] Error reloading page:`, error?.message ); } console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`); resolve(null); }, timeout); this.page_context.on("response", onResponse); }); } async getName() { try { if (!this.page_context) return null; await this.page_context.waitForSelector(".product-name", { timeout: 3000, }); return await this.page_context.evaluate(() => { const el = document.querySelector(".product-name"); return el ? el.innerText : null; }); } catch (error) { return null; } } async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price, model, }) { const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0, model, }); if (response) { this.lot_id = response.lot_id; this.close_time = response.close_time; this.start_bid_time = response.start_bid_time; } } update = async () => { if (!this.page_context) return; console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`); // 📌 Lấy thời gian kết thúc đấu giá từ giao diện const close_time = await this.getCloseTime(); console.log(`⏳ [${this.id}] Retrieved close time: ${close_time}`); // 📌 Lấy tên sản phẩm hoặc thông tin liên quan const name = await this.getName(); console.log(`📌 [${this.id}] Retrieved name: ${name}`); // 📌 Chờ phản hồi API từ trang, tối đa 10 giây const result = await this.waitForApiResponse(); // 📌 Nếu không có dữ liệu trả về thì dừng if (!result) { console.log(`⚠️ [${this.id}] No valid data received, skipping update.`); return; } // 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết const data = removeFalsyValues( { model: result?.pid || null, lot_id: result?.lotId || null, reserve_price: result.lotData?.minimumBid || null, current_price: result.lotData?.currentMaxBid || null, close_time: close_time ? String(close_time) : null, // close_time: close_time && !this.close_time ? String(close_time) : null, name, }, // [], ["close_time"] ); console.log(`🚀 [${this.id}] Processed data ready for update`); // 📌 Gửi dữ liệu cập nhật lên hệ thống await this.handleUpdateBid(data); console.log("✅ Update successful!"); return { ...response, name, close_time }; }; async getContinueShopButton() { try { if (!this.page_context) return null; await this.page_context.waitForSelector( ".btn.btn-block.btn-primary.error.continue-shopping", { timeout: 3000 } ); return await this.page_context.evaluate(() => { const el = document.querySelector( ".btn.btn-block.btn-primary.error.continue-shopping" ); return el; }); } catch (error) { return null; } } async handlePlaceBid() { if (!this.page_context) { console.log( `⚠️ [${this.id}] No page context found, aborting bid process.` ); return; } const page = this.page_context; if (global[`IS_PLACE_BID-${this.id}`]) { console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`); return; } try { console.log(`🔄 [${this.id}] Starting bid process...`); global[`IS_PLACE_BID-${this.id}`] = true; await this.delayForAction(); // start record await this.startRecordSandbox(); const continueShopBtn = await this.getContinueShopButton(); if (continueShopBtn) { console.log( `⚠️ [${this.id}] Outbid detected, calling outBid function.` ); await outBid(this.id); return; } // Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm if (this.current_price > this.max_price + this.plus_price) { console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép return; // Dừng hàm nếu giá đã vượt qua giới hạn } // Kiểm tra thời gian bid if (this.start_bid_time && !isTimeReached(this.start_bid_time)) { console.log( `⏳ [${this.id}] Not yet time to bid. Skipping Product: ${ this.name || "None" }` ); return; } // Đợi phản hồi từ API const response = await this.waitForApiResponse(); // Kiểm tra nếu phản hồi không tồn tại hoặc nếu giá đấu của người dùng bằng với giá tối đa hiện tại if ( !response || (response?.lotData?.myBid && response.lotData.myBid == this.max_price) || response?.lotData?.minimumBid > this.max_price ) { console.log( `⚠️ [${this.id}] No response or myBid equals max_price:`, response ); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa return; // Nếu không có phản hồi hoặc giá đấu bằng giá tối đa thì dừng hàm } // Kiểm tra nếu dữ liệu trong response có tồn tại và trạng thái đấu giá (bidStatus) không phải là 'None' if ( response.lotData && response.lotData?.bidStatus !== "None" && this.max_price == response?.lotData.myBid ) { console.log( `✔️ [${this.id}] Bid status is not 'None'. Current bid status:`, response.lotData?.bidStatus ); // Ghi log nếu trạng thái đấu giá không phải 'None' return; // Nếu trạng thái đấu giá không phải là 'None', dừng hàm } const bidHistoriesItem = _.maxBy(this.histories, "price"); console.log(`📜 [${this.id}] Current bid history:`, this.histories); if ( bidHistoriesItem && bidHistoriesItem?.price === this.current_price && this.max_price == response?.lotData.myBid ) { console.log( `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})` ); return; } console.log( `💰 [${this.id}] Placing a bid with amount: ${this.reserve_price}` ); // 📌 Làm rỗng ô input trước khi nhập giá đấu await page.evaluate(() => { document.querySelector("#place-bid").value = ""; }); console.log(`📝 [${this.id}] Cleared bid input field.`); // 📌 Nhập giá đấu vào ô input await page.type("#place-bid", String(this.max_price), { delay: 800 }); console.log(`✅ [${this.id}] Entered bid amount: ${this.max_price}`); // 📌 Lấy giá trị thực tế từ ô input sau khi nhập const bidValue = await page.evaluate( () => document.querySelector("#place-bid").value ); console.log(`🔍 Entered bid value: ${bidValue}`); // 📌 Kiểm tra nếu giá trị nhập vào không khớp với giá trị mong muốn if (!bidValue || bidValue !== String(this.max_price)) { console.log(`❌ Incorrect bid amount! Received: ${bidValue}`); return; // Dừng thực hiện nếu giá trị nhập sai } // 📌 Nhấn nút "Place Bid" if (this.isSandbox()) { await this.handlePlaceBidSandbox(); } else { await this.handlePlaceBidLive(); } } catch (error) { console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`); } finally { console.log(`🔚 [${this.id}] Resetting bid flag.`); global[`IS_PLACE_BID-${this.id}`] = false; // stop record this.stopRecordSandbox(); } } async handlePlaceBidLive() { const page = this.page_context; try { await page.click( ".place-bid-submit .btn.btn-primary.btn-block.place-bid-btn", { delay: 5000 } ); console.log(`🖱️ [${this.id}] Clicked "Place Bid" button.`); console.log(`📩 [${this.id}] Bid submitted, waiting for navigation...`); // 📌 Chờ trang load lại để cập nhật trạng thái đấu giá await page.waitForNavigation({ timeout: 8000, waitUntil: "domcontentloaded", }); console.log(`🔄 [${this.id}] Page reloaded, checking bid status...`); const { lotData } = await this.waitForApiResponse(); console.log(`📡 [${this.id}] API Response received:`, lotData); // 📌 Kiểm tra trạng thái đấu giá từ API if (lotData?.myBid == this.max_price) { console.log(`📸 [${this.id}] Taking bid success snapshot...`); await takeSnapshot( page, this, "bid-success", CONSTANTS.TYPE_IMAGE.SUCCESS ); // sendMessage(this); console.log(`✅ [${this.id}] Bid placed successfully!`); return; } console.log( `⚠️ [${this.id}] Bid action completed, but status is still "None".` ); } catch (error) { console.log(`[${this.id}] Error handlePlaceBidLive: ${error}`); return; } } async handlePlaceBidSandbox() { if (!this.page_context) return; console.log("🔧 Starting to update the form action for sandbox mode..."); const result = await this.setFormAction(); if (!result) { console.error("❌ Failed to update the form action for sandbox mode."); return; } console.log( "✅ Form action successfully updated. Proceeding to place the bid..." ); await this.handlePlaceBidLive(); await this.close(); } async setFormAction(newActionUrl = this.ACTION_URL()) { try { // Thay đổi action của form await this.page_context.evaluate( (url, record_url) => { const form = document.querySelector('form[name="place-bid-form"]'); if (form) { form.action = url; const hiddenInput = document.createElement("input"); hiddenInput.type = "hidden"; hiddenInput.name = "record_url"; hiddenInput.value = record_url; form.appendChild(hiddenInput); } }, newActionUrl, `${process.env.BASE_URL}admin/bids/record/${this.name_record}` ); // Kiểm tra lại giá trị action sau khi đổi const actualAction = await this.page_context.evaluate(() => { const form = document.querySelector('form[name="place-bid-form"]'); return form?.action || null; }); // Log kết quả if (actualAction === newActionUrl) { return true; } else { return false; } } catch (error) { console.log(error); } } async handleCreateLogsOnServer(data) { const values = data.map((item) => { return { model: item.pid, lot_id: item.lotId, out_price: item.lotData.minimumBid || 0, raw_data: JSON.stringify(item), }; }); await createOutBidLog(values); } async gotoLink() { const page = this.page_context; if (page.isClosed()) { console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`); return; } console.log(`🔄 [${this.id}] Starting the bidding process...`); try { console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`); await page.goto(this.url, { waitUntil: "networkidle2" }); console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`); console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`); await page.bringToFront(); console.log(`🛠️ [${this.id}] Setting custom user agent...`); 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" ); console.log(`🎯 [${this.id}] Listening for API responses...`); // // 🔥 Xóa tất cả event chặn request trước khi thêm mới // page.removeAllListeners('request'); // await page.setRequestInterception(true); // page.on('request', (request) => { // if (request.url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) { // console.log('🚀 Fake response cho request:', request.url()); // const fakeData = fs.readFileSync('./data/fake-out-lot-langtons.json', 'utf8'); // request.respond({ // status: 200, // contentType: 'application/json', // body: fakeData, // }); // } else { // try { // request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn // } catch (error) { // console.error('⚠️ Lỗi khi tiếp tục request:', error.message); // } // } // }); const onResponse = async (response) => { const url = response?.request()?.url(); if ( !url || !url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING) ) { return; } try { const { lotData, ...prev } = await response.json(); console.log(`📜 [${this.id}] Received lotData:`, lotData); if (!lotData || lotData.lotId !== this.lot_id) { console.log( `⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}` ); if (!this.page_context.isClosed()) { await this.page_context.reload({ waitUntil: "networkidle0" }); } console.log(`🔁 [${this.id}] Reload page in gotoLink`); return; } console.log(`🔍 [${this.id}] Checking bid status...`); if (["Outbid"].includes(lotData?.bidStatus)) { console.log( `⚠️ [${this.id}] Outbid detected, attempting to place a new bid...` ); this.handleCreateLogsOnServer([{ lotData, ...prev }]); } else if (["Winning"].includes(lotData?.bidStatus)) { const bidHistoriesItem = _.maxBy(this.histories, "price"); if ( !bidHistoriesItem || bidHistoriesItem?.price != lotData?.currentMaxBid ) { pushPrice({ bid_id: this.id, price: lotData?.currentMaxBid, }); } } if ( lotData.myBid && this.max_price && this.max_price != lotData.myBid ) { this.handlePlaceBid(); } } catch (error) { console.error(`🚨 [${this.id}] Error parsing API response:`, error); } }; console.log(`🔄 [${this.id}] Removing previous response listeners...`); this.page_context.off("response", onResponse); console.log(`📡 [${this.id}] Attaching new response listener...`); this.page_context.on("response", onResponse); console.log(`✅ [${this.id}] Navigation setup complete.`); } catch (error) { console.error(`❌ [${this.id}] Error during navigation:`, error); } } action = async () => { try { const page = this.page_context; // 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu if (!page.url() || !page.url().includes(this.url)) { console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`); await this.gotoLink(); } await this.handlePlaceBid(); } catch (error) { console.error(`🚨 [${this.id}] Error navigating the page: ${error}`); } }; }