diff --git a/auto-bid-admin/src/components/bid/show-histories-bid-grays-api-modal.tsx b/auto-bid-admin/src/components/bid/show-histories-bid-grays-api-modal.tsx index 103de64..76b9cdb 100644 --- a/auto-bid-admin/src/components/bid/show-histories-bid-grays-api-modal.tsx +++ b/auto-bid-admin/src/components/bid/show-histories-bid-grays-api-modal.tsx @@ -2,9 +2,8 @@ import { LoadingOverlay, Modal, ModalProps, Table } from '@mantine/core'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { getDetailBidHistories } from '../../apis/bid-histories'; -import { extractNumber } from '../../lib/table/ultils'; import { IBid } from '../../system/type'; -import { formatTime } from '../../utils'; +import { extractNumber, formatTime } from '../../utils'; export interface IShowHistoriesBidGraysApiModalProps extends ModalProps { data: IBid | null; diff --git a/auto-bid-admin/src/components/dashboard/working-page.tsx b/auto-bid-admin/src/components/dashboard/working-page.tsx index 50f9d43..89359c9 100644 --- a/auto-bid-admin/src/components/dashboard/working-page.tsx +++ b/auto-bid-admin/src/components/dashboard/working-page.tsx @@ -6,9 +6,8 @@ import { Socket } from "socket.io-client"; import { getImagesWorking } from "../../apis/bid"; import { useStatusToolStore } from "../../lib/zustand/use-status-tool-store"; import { IBid, IWebBid } from "../../system/type"; -import { cn, extractDomainSmart, stringToColor } from "../../utils"; +import { cn, extractDomainSmart, findNearestClosingChild, isTimeReached, stringToColor, subtractSeconds } from "../../utils"; import ShowImageModal from "./show-image-modal"; -import { isTimeReached, subtractSeconds } from "../../lib/table/ultils"; export interface IWorkingPageProps { data: (IBid | IWebBid) & { type: string }; socket: Socket; @@ -111,6 +110,11 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + + if(!isIBid(data)){ + console.log(data) + } + return ( <> {`Current price: $${data.current_price}`} )} + + {moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")} + + {!isIBid(data) && {`TT: ${moment(subtractSeconds(findNearestClosingChild(data)?.close_time || '', data.early_tracking_seconds)).format( + "HH:mm:ss DD/MM/YYYY" + )}`}} + {isIBid(data) && ( 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 59d16fd..dd4b5f8 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 @@ -14,7 +14,7 @@ import { z } from "zod"; import { createWebBid, updateWebBid } from "../../apis/web-bid"; import { useConfirmStore } from "../../lib/zustand/use-confirm"; import { IWebBid } from "../../system/type"; -import { extractDomain } from "../../utils"; +import { extractDomain, formatTimeFromMinutes } from "../../utils"; export interface IWebBidModelProps extends ModalProps { data: IWebBid | null; onUpdated?: () => void; @@ -158,8 +158,8 @@ export default function WebBidModal({ className="col-span-2" size="sm" label={`Arrival offset seconds (${ - form.getValues()["arrival_offset_seconds"] / 60 - } minutes)`} + formatTimeFromMinutes(form.getValues()["arrival_offset_seconds"] / 60) + })`} placeholder="msg: 300" {...form.getInputProps("arrival_offset_seconds")} /> @@ -168,8 +168,8 @@ export default function WebBidModal({ className="col-span-2" size="sm" label={`Early tracking seconds (${ - form.getValues()["early_tracking_seconds"] / 60 - } minutes)`} + formatTimeFromMinutes(form.getValues()["early_tracking_seconds"] / 60) + })`} placeholder="msg: 600" {...form.getInputProps("early_tracking_seconds")} /> diff --git a/auto-bid-admin/src/lib/table/ultils/index.ts b/auto-bid-admin/src/lib/table/ultils/index.ts index e113d5f..fa9a214 100644 --- a/auto-bid-admin/src/lib/table/ultils/index.ts +++ b/auto-bid-admin/src/lib/table/ultils/index.ts @@ -117,28 +117,4 @@ export const removeFalsy = (data: { [key: string]: string | number }) => { }, {} as { [key: string]: string | number }); }; -export function extractNumber(str: string) { - const match = str.match(/\d+(\.\d+)?/); - return match ? parseFloat(match[0]) : null; -} -export function subtractMinutes(time: string, minutes: number) { - const date = new Date(time); - date.setMinutes(date.getMinutes() - minutes); - return date.toUTCString(); -} - -export function subtractSeconds(time: string, seconds: number) { - const date = new Date(time); - date.setSeconds(date.getSeconds() - seconds); - return date.toUTCString(); -} - -export function isTimeReached(targetTime: string) { - if (!targetTime) return false; - - const targetDate = new Date(targetTime); - const now = new Date(); - - return now >= targetDate; -} diff --git a/auto-bid-admin/src/pages/bids.tsx b/auto-bid-admin/src/pages/bids.tsx index 9966fd7..36b5584 100644 --- a/auto-bid-admin/src/pages/bids.tsx +++ b/auto-bid-admin/src/pages/bids.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from "@mantine/core"; +import { ActionIcon, Anchor, Badge, Box, Menu, Text, Tooltip } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { IconAd, @@ -7,7 +7,7 @@ import { IconHammer, IconHistory, IconMenu, - IconTrash, + IconTrash } from "@tabler/icons-react"; import _ from "lodash"; import { useMemo, useRef, useState } from "react"; @@ -51,6 +51,11 @@ export default function Bids() { key: "name", title: "Name", typeFilter: "text", + renderRow(row) { + + + return {row.name} + }, }, { key: "web_bid", @@ -187,9 +192,7 @@ export default function Bids() { const table = useMemo(() => { return ( { - window.open(row.url, "_blank"); - }} + tableChildProps={{ trbody: { className: "cursor-pointer", diff --git a/auto-bid-admin/src/system/type/index.ts b/auto-bid-admin/src/system/type/index.ts index 8813a36..f486eb8 100644 --- a/auto-bid-admin/src/system/type/index.ts +++ b/auto-bid-admin/src/system/type/index.ts @@ -44,6 +44,7 @@ export interface IWebBid extends ITimestamp { active: boolean; arrival_offset_seconds: number; early_tracking_seconds: number; + snapshot_at: string | null children: IBid[]; } diff --git a/auto-bid-admin/src/utils/index.ts b/auto-bid-admin/src/utils/index.ts index 7cb4a9d..c229afb 100644 --- a/auto-bid-admin/src/utils/index.ts +++ b/auto-bid-admin/src/utils/index.ts @@ -4,6 +4,8 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import moment from "moment"; import { IWebBid } from "../system/type"; +import _ from 'lodash' + export function cn(...args: ClassValue[]) { return twMerge(clsx(args)); } @@ -201,3 +203,63 @@ export function extractDomainSmart(url: string) { return url; } } + + +export function findNearestClosingChild(webBid: IWebBid) { + 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; +} + +export function extractNumber(str: string) { + const match = str.match(/\d+(\.\d+)?/); + return match ? parseFloat(match[0]) : null; +} + +export function subtractMinutes(time: string, minutes: number) { + const date = new Date(time); + date.setMinutes(date.getMinutes() - minutes); + return date.toUTCString(); +} + +export function subtractSeconds(time: string, seconds: number) { + const date = new Date(time); + date.setSeconds(date.getSeconds() - seconds); + return date.toUTCString(); +} + +export function isTimeReached(targetTime: string) { + if (!targetTime) return false; + + const targetDate = new Date(targetTime); + const now = new Date(); + + return now >= targetDate; +} + +export function formatTimeFromMinutes(minutes: number): string { + // Tính ngày, giờ, phút từ số phút + const days = Math.floor(minutes / (60 * 24)); + const hours = Math.floor((minutes % (60 * 24)) / 60); + const mins = minutes % 60; + + let result = ''; + + if (days > 0) result += `${days} ${days > 1? 'days' :'day'} `; + if (hours > 0) result += `${hours} ${hours > 1 ? 'hours' : 'hour'} `; + if (mins > 0 || result === '') result += `${mins} minutes`; + + return result.trim(); +} diff --git a/auto-bid-server/.gitignore b/auto-bid-server/.gitignore index f9e465f..931ff8b 100644 --- a/auto-bid-server/.gitignore +++ b/auto-bid-server/.gitignore @@ -3,6 +3,7 @@ /node_modules /build /public +/bot-data # Logs logs diff --git a/auto-bid-server/bot-data/metadata.json b/auto-bid-server/bot-data/metadata.json index dd66a41..1e2c0d4 100644 --- a/auto-bid-server/bot-data/metadata.json +++ b/auto-bid-server/bot-data/metadata.json @@ -1 +1 @@ -{"createdAt":1746603511532} \ No newline at end of file +{"createdAt":1747011314493} \ No newline at end of file diff --git a/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts b/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts index ebc0e86..712d6df 100644 --- a/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts +++ b/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts @@ -23,6 +23,9 @@ export class WebBid extends Timestamp { @Column({ default: 600 }) early_tracking_seconds: number; + @Column({ default: null }) + snapshot_at: Date | null; + @Column({ default: null, nullable: true }) @Exclude() password: string; diff --git a/auto-bid-server/src/modules/bids/services/bids.service.ts b/auto-bid-server/src/modules/bids/services/bids.service.ts index c1da5de..a738ae4 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -228,7 +228,10 @@ export class BidsService { if (!bid.close_time && !bid.start_bid_time) { // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng // bid.start_bid_time = new Date().toUTCString(); - bid.start_bid_time = subtractMinutes(close_time, bid.web_bid.arrival_offset_seconds/ 60); + bid.start_bid_time = subtractMinutes( + close_time, + bid.web_bid.arrival_offset_seconds / 60, + ); } // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc) @@ -274,7 +277,7 @@ export class BidsService { const result = await this.bidsRepo.save({ ...bid, ...data, - current_price: Math.max(data?.current_price || 0, bid.current_price), + current_price: Math.max(data?.current_price || 0, bid.current_price), updated_at: new Date(), // Cập nhật timestamp }); @@ -422,6 +425,11 @@ export class BidsService { }), ); + // update time snapshot for API BID + if (type === 'API_BID') { + this.webBidsService.webBidRepo.update(id, { snapshot_at: new Date() }); + } + this.eventEmitter.emit(`working`, { status: 're-update', id, @@ -510,11 +518,9 @@ export class BidsService { return AppResponse.toResponse(files); } - async emitLoginStatus(data: ClientUpdateLoginStatusDto){ + async emitLoginStatus(data: ClientUpdateLoginStatusDto) { + this.eventEmitter.emit(Event.statusLogin(data.data), data); - this.eventEmitter.emit(Event.statusLogin(data.data), data) - - - return AppResponse.toResponse(true) + return AppResponse.toResponse(true); } } diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js index 99180ad..c7573ee 100644 --- a/auto-bid-tool/index.js +++ b/auto-bid-tool/index.js @@ -13,7 +13,7 @@ import browser from "./system/browser.js"; import configs from "./system/config.js"; import { delay, - isPageAvailable, + findNearestClosingChild, isTimeReached, safeClosePage, subtractSeconds, @@ -270,20 +270,20 @@ const clearLazyTab = async () => { // product tabs const productTabs = _.flatMap(MANAGER_BIDS, "children"); - for (const item of [...productTabs, ...MANAGER_BIDS]) { - if (!item.page_context) continue; + // for (const item of [...productTabs, ...MANAGER_BIDS]) { + // if (!item.page_context) continue; - try { - const avalableResult = await isPageAvailable(item.page_context); + // try { + // const avalableResult = await isPageAvailable(item.page_context); - if (!avalableResult) { - await safeClosePage(item); - } - } catch (e) { - console.warn("⚠️ Error checking page_context.title()", e.message); - await safeClosePage(item); - } - } + // if (!avalableResult) { + // await safeClosePage(item); + // } + // } catch (e) { + // console.warn("⚠️ Error checking page_context.title()", e.message); + // await safeClosePage(item); + // } + // } for (const page of pages) { try { @@ -303,13 +303,6 @@ const clearLazyTab = async () => { productTab?.web_bid?.early_tracking_seconds || 0 ); - console.log("%cindex.js:291 {object}", "color: #007acc;", { - earlyTrackingTime, - it_time: isTimeReached(earlyTrackingTime), - close_time: productTab.close_time, - tracking_time: productTab?.web_bid?.early_tracking_seconds, - }); - if (!isTimeReached(earlyTrackingTime)) { await safeClosePage(productTab); console.log(`🛑 Unused page detected: ${pageUrl}`); @@ -354,6 +347,15 @@ const clearLazyTab = async () => { console.warn(`⚠️ Error handling page: ${pageErr.message}`); } } + + // Delete lazy tracking page + Promise.allSettled( + MANAGER_BIDS.map(async (item) => { + if (await item.isLazy()) { + safeClosePage(item); + } + }) + ); } catch (err) { console.error("❌ Error in clearLazyTab:", err.message); } diff --git a/auto-bid-tool/models/api-bid.js b/auto-bid-tool/models/api-bid.js index d389470..1a16f3c 100644 --- a/auto-bid-tool/models/api-bid.js +++ b/auto-bid-tool/models/api-bid.js @@ -5,10 +5,12 @@ import browser from "../system/browser.js"; import CONSTANTS from "../system/constants.js"; import { findEarlyLoginTime, + findNearestClosingChild, getPathLocalData, getPathProfile, isTimeReached, sanitizeFileName, + subtractSeconds, } from "../system/utils.js"; import { Bid } from "./bid.js"; @@ -24,6 +26,8 @@ export class ApiBid extends Bid { // browser_context; username; password; + early_tracking_seconds; + snapshot_at; constructor({ url, @@ -35,6 +39,8 @@ export class ApiBid extends Bid { updated_at, origin_url, active, + early_tracking_seconds, + snapshot_at, }) { super(BID_TYPE.API_BID, url); @@ -45,6 +51,8 @@ export class ApiBid extends Bid { this.active = active; this.username = username; this.password = password; + this.early_tracking_seconds = early_tracking_seconds; + this.snapshot_at = snapshot_at; this.id = id; } @@ -58,6 +66,8 @@ export class ApiBid extends Bid { updated_at, origin_url, active, + early_tracking_seconds, + snapshot_at, }) { this.created_at = created_at; this.updated_at = updated_at; @@ -67,6 +77,8 @@ export class ApiBid extends Bid { this.username = username; this.password = password; this.url = url; + this.early_tracking_seconds = early_tracking_seconds; + this.snapshot_at = snapshot_at; } puppeteer_connect = async () => { @@ -79,12 +91,81 @@ export class ApiBid extends Bid { await this.restoreContext(); }; + async handlePrevListen() { + console.log(`👂 [${this.id}] Start handlePrevListen...`); + + // Chỉ bắt đầu check khi ảnh đã được chụp + if (this.snapshot_at) { + const nearestCloseTime = findNearestClosingChild(this); + + if (!nearestCloseTime) { + console.log(`❌ [${this.id}] No nearest closing child found.`); + return false; + } + + const { close_time } = nearestCloseTime; + console.log(`📅 [${this.id}] Nearest close_time: ${close_time}`); + + const timeToTracking = subtractSeconds( + close_time, + this.early_tracking_seconds || 0 + ); + + console.log( + `🕰️ [${this.id}] Time to tracking: ${new Date( + timeToTracking + ).toISOString()}` + ); + + if (!isTimeReached(timeToTracking)) { + console.log(`⏳ [${this.id}] Not time to track yet.`); + return false; + } + } + + 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; + } + + async isLazy() { + // Nếu chưa có ảnh chụp working => tab not lazy + if (!this.snapshot_at) return false; + + const nearestCloseTime = findNearestClosingChild(this); + + // Nếu không có nearest close => tab lazy + if (!nearestCloseTime) return true; + + const { close_time } = nearestCloseTime; + + const timeToTracking = subtractSeconds( + close_time, + this.early_tracking_seconds || 0 + ); + + // Nếu chưa đến giờ tracking => tab lazy + if (!isTimeReached(timeToTracking)) return true; + + // Các trường hợp còn lại => not lazy + return false; + } + listen_events = async () => { if (this.page_context) return; - await this.puppeteer_connect(); + // await this.puppeteer_connect(); - await this.action(); + // await this.action(); + + const results = await this.handlePrevListen(); + + if (!results) return; await this.saveContext(); }; @@ -107,16 +188,16 @@ export class ApiBid extends Bid { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); - console.log(`📂 Save at folder: ${dirPath}`); + console.log(`📂 [${this.id}] Save at folder: ${dirPath}`); } fs.writeFileSync( path.join(dirPath, sanitizeFileName(this.origin_url) + ".json"), JSON.stringify(contextData, null, 2) ); - console.log("✅ Context saved!"); + console.log(`✅ [${this.id}] Context saved!`); } catch (error) { - console.log("Save Context: ", error.message); + console.log(`[${this.id}] Save Context: , ${error.message}`); } } @@ -132,7 +213,7 @@ export class ApiBid extends Bid { // Restore Cookies await this.page_context.setCookie(...contextData.cookies); - console.log("🔄 Context restored!"); + console.log(`🔄 [${this.id}] Context restored!`); } async onCloseLogin() {} diff --git a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js index 84c6771..5b8d225 100644 --- a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js +++ b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js @@ -113,7 +113,7 @@ export class LangtonsApiBid extends ApiBid { return { success: false, error: error.toString() }; } }, - { usernam: this.username, password: this.password }, + { username: this.username, password: this.password }, csrfToken ); // truyền biến vào page context @@ -158,34 +158,14 @@ export class LangtonsApiBid extends ApiBid { } } - async submitPrevCode() { + async submitCode({ name, code }) { try { - const prevCodeData = await this.loadCodeFromLocal(); - - if (!prevCodeData) return false; - - await this.page_context.goto( - "https://www.langtons.com.au/account/mfalogin?rurl=5" - ); - - await this.page_context.waitForNavigation({ - timeout: 8000, - waitUntil: "domcontentloaded", - }); - - if (!(await page.type("#code", code, { delay: 120 }))) { - await this.clearCodeFromLocal(); - - await this.page_context.goto(configs.WEB_URLS.LANGTONS.LOGIN_URL); - return false; - } - const csrfToken = await this.getCsrfToken(); if (!csrfToken) return false; const responsePrevCode = await this.callSubmitPrevCodeApi( - prevCodeData.code, + code, csrfToken ); @@ -199,9 +179,6 @@ export class LangtonsApiBid extends ApiBid { return false; } - // submit tiếp api login.... - await this.page_context.goto(this.url); - return true; } catch (error) { console.error(`❌ [${this.id}] Error submitPrevCode:`, error); @@ -310,24 +287,31 @@ export class LangtonsApiBid extends ApiBid { // 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 }); + // // ⌨ 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 }); + // // 🚀 Click the verification confirmation button + // console.log( + // `🔘 [${this.id}] Clicking the verification confirmation button` + // ); + // await page.click(".btn.btn-block.btn-primary", { delay: 90 }); - // ⏳ Wait for navigation after verification - console.log( - `⏳ [${this.id}] Waiting for navigation after verification...` - ); - await page.waitForNavigation({ - timeout: 15000, - waitUntil: "domcontentloaded", - }); + 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" }); @@ -441,8 +425,11 @@ export class LangtonsApiBid extends ApiBid { listen_events = async () => { if (this.page_context) return; - await this.puppeteer_connect(); - await this.action(); + // await this.puppeteer_connect(); + // await this.action(); + const results = await this.handlePrevListen(); + + if (!results) return; this.reloadInterval = setInterval(async () => { try { diff --git a/auto-bid-tool/models/lawsons.com.au/lawsons-api-bid.js b/auto-bid-tool/models/lawsons.com.au/lawsons-api-bid.js index fed52c6..e42d16b 100644 --- a/auto-bid-tool/models/lawsons.com.au/lawsons-api-bid.js +++ b/auto-bid-tool/models/lawsons.com.au/lawsons-api-bid.js @@ -1,258 +1,300 @@ -import fs from 'fs'; -import configs from '../../system/config.js'; -import { delay, getPathProfile, safeClosePage } from '../../system/utils.js'; -import { ApiBid } from '../api-bid.js'; +import fs from "fs"; +import configs from "../../system/config.js"; +import { delay, getPathProfile, safeClosePage } from "../../system/utils.js"; +import { ApiBid } from "../api-bid.js"; export class LawsonsApiBid extends ApiBid { - reloadInterval = null; - constructor({ ...prev }) { - super(prev); + 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 2 minute.` + ) + ); + }, 120 * 1000); // 60 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 + }); + }); + + async isLogin() { + if (!this.page_context) return false; + + const filePath = getPathProfile(this.origin_url); + + return ( + !(await this.page_context.$("#emailLogin")) && fs.existsSync(filePath) + ); + } + + 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.` + ) + ); + }, 60 * 1000); // 60 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 + }); + }); + + async enterOTP(otp) { + try { + // Selector cho tất cả các input OTP + const inputSelector = ".MuiDialog-container .container input"; + + // Chờ cho các input OTP xuất hiện + await this.page_context.waitForSelector(inputSelector, { timeout: 8000 }); + + // Lấy tất cả các input OTP + const inputs = await this.page_context.$$(inputSelector); + + // Kiểm tra nếu có đúng 6 trường input + if (inputs.length === 6 && otp.length === 6) { + // Nhập mỗi ký tự của OTP vào các input tương ứng + for (let i = 0; i < 6; i++) { + await inputs[i].type(otp[i], { delay: 100 }); + } + console.log(`✅ OTP entered successfully: ${otp}`); + } else { + console.error("❌ Invalid OTP or input fields count"); + } + } catch (error) { + console.error("❌ Error entering OTP:", error); + } + } + + async waitToTwoVerify() { + try { + if (!this.page_context) return false; + + // Selector của các phần tử trên trang + const button = ".form-input-wrapper.form-group > .btn.btn-primary"; // Nút để tiếp tục quá trình xác minh + const remember = ".PrivateSwitchBase-input"; // Checkbox "Remember me" + const continueButton = + ".MuiButtonBase-root.MuiButton-root.MuiButton-contained.MuiButton-containedPrimary.MuiButton-sizeMedium.MuiButton-containedSizeMedium.MuiButton-colorPrimary.MuiButton-root"; // Nút "Continue" + + // Chờ cho nút xác minh xuất hiện + console.log( + `🔎 [${this.id}] Waiting for the button with selector: ${button}` + ); + await this.page_context.waitForSelector(button, { timeout: 8000 }); + + console.log(`✅ [${this.id}] Button found, clicking the first button.`); + + // Lấy phần tử của nút và log nội dung của nó + const firstButton = await this.page_context.$(button); // Lấy phần tử đầu tiên + const buttonContent = await firstButton.evaluate((el) => el.textContent); // Lấy nội dung của nút + console.log(`🔎 [${this.id}] Button content: ${buttonContent}`); + + // Chờ 2s cho button sẵn sàn + await delay(2000); + // Click vào nút xác minh + await firstButton.click(); + console.log(`✅ [${this.id}] Button clicked.`); + + // Nhận mã OTP để nhập vào form + const { name, code } = await this.waitVerifyData(); + console.log( + `🔎 [${this.id}] Waiting for OTP input, received code: ${code}` + ); + + // Nhập mã OTP vào form + await this.enterOTP(code); + console.log(`✅ [${this.id}] OTP entered successfully.`); + + // Chờ cho checkbox "Remember me" xuất hiện + await this.page_context.waitForSelector(remember, { timeout: 8000 }); + console.log( + `🔎 [${this.id}] Waiting for remember me checkbox with selector: ${remember}` + ); + + // Click vào checkbox "Remember me" + await this.page_context.click(remember, { delay: 92 }); + console.log(`✅ [${this.id}] Remember me checkbox clicked.`); + + // Chờ cho nút "Continue" xuất hiện + await this.page_context.waitForSelector(continueButton, { + timeout: 8000, + }); + console.log( + `🔎 [${this.id}] Waiting for continue button with selector: ${continueButton}` + ); + + // Click vào nút "Continue" + await this.page_context.click(continueButton, { delay: 100 }); + console.log(`✅ [${this.id}] Continue button clicked.`); + + // Chờ cho trang tải hoàn tất sau khi click "Continue" + await this.page_context.waitForNavigation({ + waitUntil: "domcontentloaded", + }); + console.log(`✅ [${this.id}] Navigation completed.`); + + return true; + } catch (error) { + console.error(`❌ [${this.id}] Error:`, 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.$("#emailLogin")) && fs.existsSync(filePath)) { + console.log(`✅ [${this.id}] Already logged in, skipping login process.`); + return; } - 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 2 minute.`)); - }, 120 * 1000); // 60 giây + if (fs.existsSync(filePath)) { + console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`); + fs.unlinkSync(filePath); + } - 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 - }); + 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); + } + + console.log(`🔑 [${this.id}] Starting login process...`); + + try { + // ⌨ Enter email + console.log(`✍ [${this.id}] Entering email:`, this.username); + await page.type("#emailLogin", this.username, { delay: 100 }); + + // ⌨ Enter password + console.log(`✍ [${this.id}] Entering password...`); + await page.type("#passwordLogin", this.password, { delay: 150 }); + + // 🚀 Click the login button + console.log(`🔘 [${this.id}] Clicking the "Login" button`); + await page.click("#signInBtn", { delay: 92 }); + + const result = await this.waitToTwoVerify(); + + // ⏳ Wait for navigation after login + if (!result) { + console.log(`⏳ [${this.id}] Waiting for navigation after login...`); + await page.waitForNavigation({ + timeout: 8000, + waitUntil: "domcontentloaded", }); + } - async isLogin() { - if (!this.page_context) return false; - - const filePath = getPathProfile(this.origin_url); - - return !(await this.page_context.$('#emailLogin')) && fs.existsSync(filePath); + if (this.page_context.url() == this.url) { + // 📂 Save session context to avoid re-login + await this.saveContext(); + console.log(`✅ [${this.id}] Login successful!`); + } else { + console.log(`❌ [${this.id}] Login Failure!`); + } + } catch (error) { + console.error( + `❌ [${this.id}] Error during login process:`, + error.message + ); + } finally { + global.IS_CLEANING = true; } + } - 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.`)); - }, 60 * 1000); // 60 giây + action = async () => { + try { + const page = this.page_context; - 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 - }); - }); - - async enterOTP(otp) { - try { - // Selector cho tất cả các input OTP - const inputSelector = '.MuiDialog-container .container input'; - - // Chờ cho các input OTP xuất hiện - await this.page_context.waitForSelector(inputSelector, { timeout: 8000 }); - - // Lấy tất cả các input OTP - const inputs = await this.page_context.$$(inputSelector); - - // Kiểm tra nếu có đúng 6 trường input - if (inputs.length === 6 && otp.length === 6) { - // Nhập mỗi ký tự của OTP vào các input tương ứng - for (let i = 0; i < 6; i++) { - await inputs[i].type(otp[i], { delay: 100 }); - } - console.log(`✅ OTP entered successfully: ${otp}`); - } else { - console.error('❌ Invalid OTP or input fields count'); - } - } catch (error) { - console.error('❌ Error entering OTP:', error); + page.on("response", async (response) => { + const request = response.request(); + if (request.redirectChain().length > 0) { + if (response.url().includes(configs.WEB_CONFIGS.LAWSONS.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); } + }; - async waitToTwoVerify() { - try { - if (!this.page_context) return false; + listen_events = async () => { + if (this.page_context) return; - // Selector của các phần tử trên trang - const button = '.form-input-wrapper.form-group > .btn.btn-primary'; // Nút để tiếp tục quá trình xác minh - const remember = '.PrivateSwitchBase-input'; // Checkbox "Remember me" - const continueButton = - '.MuiButtonBase-root.MuiButton-root.MuiButton-contained.MuiButton-containedPrimary.MuiButton-sizeMedium.MuiButton-containedSizeMedium.MuiButton-colorPrimary.MuiButton-root'; // Nút "Continue" + // await this.puppeteer_connect(); + // await this.action(); - // Chờ cho nút xác minh xuất hiện - console.log(`🔎 [${this.id}] Waiting for the button with selector: ${button}`); - await this.page_context.waitForSelector(button, { timeout: 8000 }); + const results = await this.handlePrevListen(); - console.log(`✅ [${this.id}] Button found, clicking the first button.`); + if (!results) return; - // Lấy phần tử của nút và log nội dung của nó - const firstButton = await this.page_context.$(button); // Lấy phần tử đầu tiên - const buttonContent = await firstButton.evaluate((el) => el.textContent); // Lấy nội dung của nút - console.log(`🔎 [${this.id}] Button content: ${buttonContent}`); - - // Chờ 2s cho button sẵn sàn - await delay(2000); - // Click vào nút xác minh - await firstButton.click(); - console.log(`✅ [${this.id}] Button clicked.`); - - // Nhận mã OTP để nhập vào form - const { name, code } = await this.waitVerifyData(); - console.log(`🔎 [${this.id}] Waiting for OTP input, received code: ${code}`); - - // Nhập mã OTP vào form - await this.enterOTP(code); - console.log(`✅ [${this.id}] OTP entered successfully.`); - - // Chờ cho checkbox "Remember me" xuất hiện - await this.page_context.waitForSelector(remember, { timeout: 8000 }); - console.log(`🔎 [${this.id}] Waiting for remember me checkbox with selector: ${remember}`); - - // Click vào checkbox "Remember me" - await this.page_context.click(remember, { delay: 92 }); - console.log(`✅ [${this.id}] Remember me checkbox clicked.`); - - // Chờ cho nút "Continue" xuất hiện - await this.page_context.waitForSelector(continueButton, { timeout: 8000 }); - console.log(`🔎 [${this.id}] Waiting for continue button with selector: ${continueButton}`); - - // Click vào nút "Continue" - await this.page_context.click(continueButton, { delay: 100 }); - console.log(`✅ [${this.id}] Continue button clicked.`); - - // Chờ cho trang tải hoàn tất sau khi click "Continue" - await this.page_context.waitForNavigation({ waitUntil: 'domcontentloaded' }); - console.log(`✅ [${this.id}] Navigation completed.`); - - return true; - } catch (error) { - console.error(`❌ [${this.id}] Error:`, error); - return false; + 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.`); + } else { + console.log( + `❌ [${this.id}] Page context is closed. Stopping reload.` + ); + clearInterval(this.reloadInterval); } - } - - 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.$('#emailLogin')) && fs.existsSync(filePath)) { - console.log(`✅ [${this.id}] Already logged in, skipping login process.`); - 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); - } - - console.log(`🔑 [${this.id}] Starting login process...`); - - try { - // ⌨ Enter email - console.log(`✍ [${this.id}] Entering email:`, this.username); - await page.type('#emailLogin', this.username, { delay: 100 }); - - // ⌨ Enter password - console.log(`✍ [${this.id}] Entering password...`); - await page.type('#passwordLogin', this.password, { delay: 150 }); - - // 🚀 Click the login button - console.log(`🔘 [${this.id}] Clicking the "Login" button`); - await page.click('#signInBtn', { delay: 92 }); - - const result = await this.waitToTwoVerify(); - - // ⏳ Wait for navigation after login - if (!result) { - console.log(`⏳ [${this.id}] Waiting for navigation after login...`); - await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }); - } - - if (this.page_context.url() == this.url) { - // 📂 Save session context to avoid re-login - await this.saveContext(); - console.log(`✅ [${this.id}] Login successful!`); - } else { - console.log(`❌ [${this.id}] Login Failure!`); - } - } catch (error) { - console.error(`❌ [${this.id}] Error during login process:`, error.message); - } finally { - global.IS_CLEANING = true; - } - } - - 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.LAWSONS.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(); - - 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.`); - } 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 - }; + } catch (error) { + console.error(`🚨 [${this.id}] Error reloading page:`, error.message); + } + }, 60000); // 1p reload + }; } diff --git a/auto-bid-tool/models/pickles.com.au/pickles-api-bid.js b/auto-bid-tool/models/pickles.com.au/pickles-api-bid.js index d0190b8..0723fa7 100644 --- a/auto-bid-tool/models/pickles.com.au/pickles-api-bid.js +++ b/auto-bid-tool/models/pickles.com.au/pickles-api-bid.js @@ -128,8 +128,12 @@ export class PicklesApiBid extends ApiBid { listen_events = async () => { if (this.page_context) return; - await this.puppeteer_connect(); - await this.action(); + // await this.puppeteer_connect(); + // await this.action(); + + const results = await this.handlePrevListen(); + + if (!results) return; this.reloadInterval = setInterval(async () => { try { diff --git a/auto-bid-tool/system/utils.js b/auto-bid-tool/system/utils.js index 2582ca2..c133227 100644 --- a/auto-bid-tool/system/utils.js +++ b/auto-bid-tool/system/utils.js @@ -2,6 +2,7 @@ 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)); @@ -303,3 +304,21 @@ export async function isPageAvailable(page) { 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; +}