Merge pull request 'update time tracking for api bid' (#29) from zelda.by-pass-langtons-prev-code into main

Reviewed-on: #29
This commit is contained in:
zelda 2025-05-12 16:51:03 +10:00
commit c19400ef66
17 changed files with 554 additions and 357 deletions

View File

@ -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;

View File

@ -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 (
<>
<Box
@ -140,10 +144,17 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) {
<Text className="text-xs tracking-wide">{`Current price: $${data.current_price}`}</Text>
)}
<Text className="text-sm italic opacity-80">
{moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")}
</Text>
{!isIBid(data) && <Tooltip label={'Time to tracking'}><Text>{`TT: ${moment(subtractSeconds(findNearestClosingChild(data)?.close_time || '', data.early_tracking_seconds)).format(
"HH:mm:ss DD/MM/YYYY"
)}`}</Text></Tooltip>}
<Box className="flex items-center gap-3">
{isIBid(data) && (
<Tooltip label={'Close time'}>

View File

@ -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")}
/>

View File

@ -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;
}

View File

@ -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 <Anchor className="text-[14px]" href={row.url} size="sm" style={{color: 'inherit'}}>{row.name}</Anchor>
},
},
{
key: "web_bid",
@ -187,9 +192,7 @@ export default function Bids() {
const table = useMemo(() => {
return (
<Table
onClickRow={(row) => {
window.open(row.url, "_blank");
}}
tableChildProps={{
trbody: {
className: "cursor-pointer",

View File

@ -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[];
}

View File

@ -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();
}

View File

@ -3,6 +3,7 @@
/node_modules
/build
/public
/bot-data
# Logs
logs

View File

@ -1 +1 @@
{"createdAt":1746603511532}
{"createdAt":1747011314493}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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() {}

View File

@ -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 {

View File

@ -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
};
}

View File

@ -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 {

View File

@ -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;
}