607 lines
19 KiB
JavaScript
607 lines
19 KiB
JavaScript
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}`);
|
|
}
|
|
};
|
|
}
|