diff --git a/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx b/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx index dd4b5f8..2fc41a5 100644 --- a/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx +++ b/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx @@ -26,12 +26,12 @@ const schema = { .number({ message: "Arrival offset seconds is required" }) .refine((val) => val >= 60, { message: "Arrival offset seconds must be at least 60 seconds (1 minute)", - }), + }).optional(), early_tracking_seconds: z .number({ message: "Early login seconds is required" }) .refine((val) => val >= 600, { message: "Early login seconds must be at least 600 seconds (10 minute)", - }), + }).optional(), }; export default function WebBidModal({ diff --git a/auto-bid-server/bot-data/metadata.json b/auto-bid-server/bot-data/metadata.json index 1e2c0d4..afd40d3 100644 --- a/auto-bid-server/bot-data/metadata.json +++ b/auto-bid-server/bot-data/metadata.json @@ -1 +1 @@ -{"createdAt":1747011314493} \ No newline at end of file +{"createdAt":1747107717780} \ No newline at end of file diff --git a/auto-bid-server/src/modules/bids/dto/web-bid/create-web-bid.ts b/auto-bid-server/src/modules/bids/dto/web-bid/create-web-bid.ts index 5fbcf3f..dceb7dd 100644 --- a/auto-bid-server/src/modules/bids/dto/web-bid/create-web-bid.ts +++ b/auto-bid-server/src/modules/bids/dto/web-bid/create-web-bid.ts @@ -1,4 +1,4 @@ -import { IsNumber, IsString, IsUrl } from 'class-validator'; +import { IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator'; export class CreateWebBidDto { @IsUrl() @@ -6,4 +6,14 @@ export class CreateWebBidDto { @IsUrl() url: string; + + @IsNumber() + @Min(60) + @IsOptional() + arrival_offset_seconds: number; + + @IsNumber() + @Min(600) + @IsOptional() + early_tracking_seconds: number; } diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js index 7065a9f..1e89bd5 100644 --- a/auto-bid-tool/index.js +++ b/auto-bid-tool/index.js @@ -184,6 +184,8 @@ const tracking = async () => { // 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}` ); diff --git a/auto-bid-tool/models/api-bid.js b/auto-bid-tool/models/api-bid.js index 1a16f3c..0a14b6c 100644 --- a/auto-bid-tool/models/api-bid.js +++ b/auto-bid-tool/models/api-bid.js @@ -98,9 +98,15 @@ export class ApiBid extends Bid { if (this.snapshot_at) { const nearestCloseTime = findNearestClosingChild(this); - if (!nearestCloseTime) { - console.log(`❌ [${this.id}] No nearest closing child found.`); - return false; + if (!nearestCloseTime || this.children.some((item) => !item.close_time)) { + console.log(`🔌 [${this.id}] Connecting to puppeteer...`); + await this.puppeteer_connect(); + + console.log(`✅ [${this.id}] Connected. Executing actions...`); + await this.action(); + + console.log(`🎯 [${this.id}] handlePrevListen completed.`); + return true; } const { close_time } = nearestCloseTime; @@ -137,10 +143,13 @@ export class ApiBid extends Bid { // Nếu chưa có ảnh chụp working => tab not lazy if (!this.snapshot_at) return false; + // Nếu có một children chưa có thông tin => tab not lazy + if (this.children.some((item) => !item.close_time)) return false; + const nearestCloseTime = findNearestClosingChild(this); - // Nếu không có nearest close => tab lazy - if (!nearestCloseTime) return true; + // Nếu không có nearest close => tab not lazy + if (!nearestCloseTime) return false; const { close_time } = nearestCloseTime; diff --git a/auto-bid-tool/models/grays.com/grays-api-bid.js b/auto-bid-tool/models/grays.com/grays-api-bid.js index bdfbbda..97c3f4f 100644 --- a/auto-bid-tool/models/grays.com/grays-api-bid.js +++ b/auto-bid-tool/models/grays.com/grays-api-bid.js @@ -125,37 +125,37 @@ export class GrayApiBid extends ApiBid { (bid) => !this.children_processing.some((item) => item.model === bid.Sku) ); - const handleChildren = this.children.filter((item) => - bidOutLots.some((i) => i.Sku === item.model) - ); + // const handleChildren = this.children.filter((item) => + // bidOutLots.some((i) => i.Sku === item.model) + // ); - console.log({ - handleChildren, - children_processing: this.children_processing, - data, - bidOutLots, - }); + // console.log({ + // handleChildren, + // children_processing: this.children_processing, + // data, + // bidOutLots, + // }); - for (const product_tab of handleChildren) { - if (!isTimeReached(product_tab.start_bid_time)) { - console.log( - `❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...` - ); - return; - } + // for (const product_tab of handleChildren) { + // if (!isTimeReached(product_tab.start_bid_time)) { + // console.log( + // `❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...` + // ); + // return; + // } - this.children_processing.push(product_tab); + // this.children_processing.push(product_tab); - if (!product_tab.page_context) { - await product_tab.puppeteer_connect(); - } + // if (!product_tab.page_context) { + // await product_tab.puppeteer_connect(); + // } - await product_tab.action(); + // await product_tab.action(); - this.children_processing = this.children_processing.filter( - (item) => item.id !== product_tab.id - ); - } + // this.children_processing = this.children_processing.filter( + // (item) => item.id !== product_tab.id + // ); + // } }; isLogin = async () => { diff --git a/auto-bid-tool/models/grays.com/grays-product-bid copy.js b/auto-bid-tool/models/grays.com/grays-product-bid copy.js new file mode 100644 index 0000000..2390dec --- /dev/null +++ b/auto-bid-tool/models/grays.com/grays-product-bid copy.js @@ -0,0 +1,322 @@ +import { + outBid, + pushPrice, + updateBid, + updateStatusByPrice, +} from "../../system/apis/bid.js"; +import CONSTANTS from "../../system/constants.js"; +import { + delay, + extractNumber, + isNumber, + isTimeReached, + removeFalsyValues, + safeClosePage, + takeSnapshot, +} from "../../system/utils.js"; +import { ProductBid } from "../product-bid.js"; + +export class GraysProductBidBackup extends ProductBid { + constructor({ ...prev }) { + super(prev); + } + + async validate({ page, price_value }) { + if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) { + console.log(`❌ [${this.id}] It's not time yet`); + return { result: false, bid_price: 0 }; + } + + if (!isNumber(price_value)) { + console.log(`❌ [${this.id}] Can't get PRICE_VALUE`); + await takeSnapshot(page, this, "price-value-null"); + + return { result: false, bid_price: 0 }; + } + + const bid_price = this.plus_price + Number(price_value); + + if (bid_price > this.max_price) { + console.log( + `❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT` + ); + await takeSnapshot(page, this, "price-bid-more-than"); + + await outBid(this.id); + + return { result: false, bid_price: 0 }; + } + + const response = await pushPrice({ + bid_id: this.id, + price: bid_price, + }); + + if (!response.status) { + return { result: false, bid_price: 0 }; + } + + this.histories = response.data; + + // RESET first bid + if (this.histories.length > 0 && this.first_bid) { + this.first_bid = false; + } + + return { result: true, bid_price }; + } + + getCloseTime = async () => { + try { + if (!this.page_context) return null; + + await this.page_context.waitForSelector("#lot-closing-datetime", { + timeout: 3000, + }); + + return await this.page_context.$eval( + "#lot-closing-datetime", + (el) => el.value + ); + } catch (error) { + return null; + } + }; + + getPriceWasBid = async () => { + try { + if (!this.page_context) return null; + + await this.page_context.waitForSelector( + "#biddableLot form div div:nth-child(1) span span", + { timeout: 3000 } + ); + + const element = await this.page_context.$( + "#biddableLot form div div:nth-child(1) span span" + ); + + const textPrice = await this.page_context.evaluate( + (el) => el.textContent, + element + ); + + return extractNumber(textPrice) || null; + } catch (error) { + return null; + } + }; + + async isCloseProduct() { + const close_time = await this.getCloseTime(); + + if (!close_time) { + const priceWasBid = await this.getPriceWasBid(); + + await updateStatusByPrice(this.id, priceWasBid); + return { result: true, close_time: null }; + } + + await delay(500); + + if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) { + console.log(`❌ [${this.id}] Product is close ${close_time}`); + return { result: true, close_time }; + } + + return { result: false, close_time }; + } + + async handleWritePrice(page, bid_price) { + await page.type("#price", String(bid_price)); + await delay(500); + } + + async placeBid(page) { + try { + await page.click("#bid-type-standard"); + await delay(500); + + await page.click("#btnSubmit"); + await delay(1000); + + await page.waitForSelector("button", { timeout: 5000 }); + + await delay(500); + + await page.click("button"); + + await page.waitForNavigation({ timeout: 5000 }); + + await takeSnapshot( + page, + this, + "bid-success", + CONSTANTS.TYPE_IMAGE.SUCCESS + ); + return true; + } catch (error) { + console.log(`❌ [${this.id}] Timeout to loading`); + await takeSnapshot(page, this, "timeout to loading"); + return false; + } + } + + async handleReturnProductPage(page) { + await page.goto(this.url); + await delay(1000); + } + + async handleUpdateBid({ + lot_id, + close_time, + name, + current_price, + reserve_price, + }) { + const response = await updateBid(this.id, { + lot_id, + close_time, + name, + current_price, + reserve_price: Number(reserve_price) || 0, + }); + + 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; + + const page = this.page_context; + + try { + const close_time = await this.getCloseTime(); + + // Chờ phần tử xuất hiện trước khi lấy giá trị + await page + .waitForSelector("#priceValue", { timeout: 5000 }) + .catch(() => null); + const price_value = await page + .$eval("#priceValue", (el) => el.value) + .catch(() => null); + + await page.waitForSelector("#lotId", { timeout: 5000 }).catch(() => null); + const lot_id = await page + .$eval("#lotId", (el) => el.value) + .catch(() => null); + + await page + .waitForSelector("#placebid-sticky > div:nth-child(2) > div > h3", { + timeout: 5000, + }) + .catch(() => null); + const name = await page + .$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText) + .catch(() => null); + + await page + .waitForSelector( + "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span", + { timeout: 5000 } + ) + .catch(() => null); + const current_price = await page + .$eval( + "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span", + (el) => el.innerText + ) + .catch(() => null); + + console.log( + `📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}` + ); + + const data = removeFalsyValues( + { + lot_id, + reserve_price: price_value, + close_time: close_time ? String(close_time) : null, + name, + current_price: current_price ? extractNumber(current_price) : null, + }, + ["close_time"] + ); + + this.handleUpdateBid(data); + + return { price_value, lot_id, name, current_price }; + } catch (error) { + console.error(`🚨 Error updating product info: ${error.message}`); + return null; + } + }; + + action = async () => { + try { + const page = this.page_context; + + await this.gotoLink(); + console.log(`🌍 [${this.id}] Navigated to link.`); + + await delay(1000); + + const { close_time, ...isCloseProduct } = await this.isCloseProduct(); + if (isCloseProduct.result) { + console.log( + `❌ [${this.id}] The product is closed, cannot place a bid.` + ); + return; + } + + await delay(500); + + const { price_value } = await this.update(); + if (!price_value) return; + + const { result, bid_price } = await this.validate({ page, price_value }); + if (!result) { + console.log( + `❌ [${this.id}] Validation failed. Unable to proceed with bidding.` + ); + return; + } + + const bidHistoriesItem = _.maxBy(this.histories, "price"); + if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) { + console.log( + `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})` + ); + return; + } + + if (price_value != bid_price) { + console.log( + `✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}` + ); + await this.handleWritePrice(page, bid_price); + } + + console.log(`🚀 [${this.id}] Placing the bid...`); + const resultPlaceBid = await this.placeBid(page); + if (!resultPlaceBid) { + console.log(`❌ [${this.id}] Error occurred while placing the bid.`); + await takeSnapshot(page, this, "place-bid-action"); + return; + } + + console.log( + `✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}` + ); + await this.handleReturnProductPage(page); + } catch (error) { + console.error( + `🚨 [${this.id}] Error navigating the page: ${error.message}` + ); + } + }; +} diff --git a/auto-bid-tool/models/grays.com/grays-product-bid.js b/auto-bid-tool/models/grays.com/grays-product-bid.js index ea1513a..7cc2cb2 100644 --- a/auto-bid-tool/models/grays.com/grays-product-bid.js +++ b/auto-bid-tool/models/grays.com/grays-product-bid.js @@ -21,51 +21,6 @@ export class GraysProductBid extends ProductBid { super(prev); } - async validate({ page, price_value }) { - if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) { - console.log(`❌ [${this.id}] It's not time yet`); - return { result: false, bid_price: 0 }; - } - - if (!isNumber(price_value)) { - console.log(`❌ [${this.id}] Can't get PRICE_VALUE`); - await takeSnapshot(page, this, "price-value-null"); - - return { result: false, bid_price: 0 }; - } - - const bid_price = this.plus_price + Number(price_value); - - if (bid_price > this.max_price) { - console.log( - `❌ ${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT` - ); - await takeSnapshot(page, this, "price-bid-more-than"); - - await outBid(this.id); - - return { result: false, bid_price: 0 }; - } - - const response = await pushPrice({ - bid_id: this.id, - price: bid_price, - }); - - if (!response.status) { - return { result: false, bid_price: 0 }; - } - - this.histories = response.data; - - // RESET first bid - if (this.histories.length > 0 && this.first_bid) { - this.first_bid = false; - } - - return { result: true, bid_price }; - } - getCloseTime = async () => { try { if (!this.page_context) return null; @@ -127,43 +82,61 @@ export class GraysProductBid extends ProductBid { return { result: false, close_time }; } - async handleWritePrice(page, bid_price) { - await page.type("#price", String(bid_price)); - await delay(500); - } - - async placeBid(page) { + async placeBid() { try { - await page.click("#bid-type-standard"); - await delay(500); + await this.page_context.evaluate(() => { + document.querySelector("#price").value = ""; + }); - await page.click("#btnSubmit"); + await this.page_context.type("#price", String(this.max_price)); + + await delay(5000); + + const currentValue = await this.page_context.$eval( + "#price", + (el) => el.value + ); + + if (currentValue !== String(this.max_price)) { + console.warn( + `[${this.id}] Value not match #price: ${currentValue} !== ${this.max_price}` + ); + return; + } + + await this.page_context.click("#btnSubmit"); await delay(1000); - await page.waitForSelector("button", { timeout: 5000 }); + await this.page_context.waitForSelector("button", { timeout: 5000 }); await delay(500); - await page.click("button"); + await this.page_context.click("button"); - await page.waitForNavigation({ timeout: 5000 }); + await this.page_context.waitForNavigation({ timeout: 5000 }); - await takeSnapshot( - page, - this, - "bid-success", - CONSTANTS.TYPE_IMAGE.SUCCESS + await this.page_context.waitForFunction( + () => document.body.innerText.includes("Successfully"), + { timeout: 5000 } // hoặc lâu hơn nếu cần ); + console.log("✅ Found 'Successfully'"); + + await pushPrice({ + bid_id: this.id, + price: this.max_price, + }); + + await this.handleReturnProductPage(); + return true; } catch (error) { - console.log(`❌ [${this.id}] Timeout to loading`); - await takeSnapshot(page, this, "timeout to loading"); + console.log(`❌ [${this.id}] Error in placeBid: ${error.message}`); return false; } } - async handleReturnProductPage(page) { - await page.goto(this.url); + async handleReturnProductPage() { + await this.page_context.goto(this.url); await delay(1000); } @@ -256,63 +229,146 @@ export class GraysProductBid extends ProductBid { } }; + getCurrentData = async () => { + if (!this.page_context) return null; + + try { + // Lấy thời gian đóng + const close_time = await this.getCloseTime(); + + // Giá trị reserve price + await this.page_context + .waitForSelector("#priceValue", { timeout: 5000 }) + .catch(() => null); + const price_value = await this.page_context + .$eval("#priceValue", (el) => el.value) + .catch(() => null); + + // Lot ID + await this.page_context + .waitForSelector("#lotId", { timeout: 5000 }) + .catch(() => null); + const lot_id = await this.page_context + .$eval("#lotId", (el) => el.value) + .catch(() => null); + + // Tên sản phẩm + await this.page_context + .waitForSelector(".dls-heading-3.lotPageTitle", { timeout: 5000 }) + .catch(() => null); + const name = await this.page_context + .$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText) + .catch(() => null); + + // Giá hiện tại + await this.page_context + .waitForSelector( + "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span", + { timeout: 5000 } + ) + .catch(() => null); + const current_price_raw = await this.page_context + .$eval( + "#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span", + (el) => el.innerText + ) + .catch(() => null); + + const current_price = current_price_raw + ? extractNumber(current_price_raw) + : null; + + return removeFalsyValues( + { + lot_id, + reserve_price: Number(price_value) || 0, + close_time: close_time ? String(close_time) : null, + name, + current_price, + }, + ["close_time"] + ); + } catch (error) { + console.error(`🚨 Error fetching current product data: ${error.message}`); + 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; + + // Tắt clearLazyTab vì web này phải navigate để bid + global.IS_CLEANING = false; + + const isCloseProduct = await this.isCloseProduct(); + if (isCloseProduct.result) { + 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; + } + + if (this.histories.length > 0) { + console.log( + `[${this.id}] Already biding with price ${this.histories[0].price}` + ); + return; + } + + const result = await this.placeBid(); + + global.IS_CLEANING = true; + global[`IS_PLACE_BID-${this.id}`] = false; + } catch (error) { + console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`); + } finally { + console.log(`🔚 [${this.id}] Resetting bid flag.`); + } + } + action = async () => { try { const page = this.page_context; - await this.gotoLink(); - console.log(`🌍 [${this.id}] Navigated to link.`); - - await delay(1000); - - const { close_time, ...isCloseProduct } = await this.isCloseProduct(); - if (isCloseProduct.result) { - console.log( - `❌ [${this.id}] The product is closed, cannot place a bid.` - ); - return; + // 📌 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 delay(500); - - const { price_value } = await this.update(); - if (!price_value) return; - - const { result, bid_price } = await this.validate({ page, price_value }); - if (!result) { - console.log( - `❌ [${this.id}] Validation failed. Unable to proceed with bidding.` - ); - return; - } - - const bidHistoriesItem = _.maxBy(this.histories, "price"); - if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) { - console.log( - `🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})` - ); - return; - } - - if (price_value != bid_price) { - console.log( - `✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}` - ); - await this.handleWritePrice(page, bid_price); - } - - console.log(`🚀 [${this.id}] Placing the bid...`); - const resultPlaceBid = await this.placeBid(page); - if (!resultPlaceBid) { - console.log(`❌ [${this.id}] Error occurred while placing the bid.`); - await takeSnapshot(page, this, "place-bid-action"); - return; - } - - console.log( - `✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}` - ); - await this.handleReturnProductPage(page); + await this.handlePlaceBid(); } catch (error) { console.error( `🚨 [${this.id}] Error navigating the page: ${error.message}`