pickxel and fix login

This commit is contained in:
Admin 2025-05-05 10:05:12 +07:00
parent b13712a317
commit 65ae7da6e6
18 changed files with 886 additions and 431 deletions

View File

@ -1,92 +1,110 @@
import { generateNestParams, handleError, handleSuccess } from '.';
import axios from '../lib/axios';
import { IWebBid } from '../system/type';
import { removeFalsyValues } from '../utils';
import { generateNestParams, handleError, handleSuccess } from ".";
import axios from "../lib/axios";
import { IWebBid } from "../system/type";
import { removeFalsyValues } from "../utils";
const BASE_URL = 'web-bids';
const BASE_URL = "web-bids";
export const getWebBids = async (params: Record<string, string | number>) => {
return await axios({
url: BASE_URL,
params: generateNestParams(params),
withCredentials: true,
method: 'GET',
});
return await axios({
url: BASE_URL,
params: generateNestParams(params),
withCredentials: true,
method: "GET",
});
};
export const createWebBid = async (bid: Omit<IWebBid, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
const newData = removeFalsyValues(bid);
export const createWebBid = async (
bid: Omit<IWebBid, "id" | "created_at" | "updated_at" | "is_system_account">
) => {
const newData = removeFalsyValues(bid);
try {
const { data } = await axios({
url: BASE_URL,
withCredentials: true,
method: 'POST',
data: newData,
});
try {
const { data } = await axios({
url: BASE_URL,
withCredentials: true,
method: "POST",
data: newData,
});
handleSuccess(data);
handleSuccess(data);
return data;
} catch (error) {
handleError(error);
}
return data;
} catch (error) {
handleError(error);
}
};
export const updateWebBid = async (bid: Partial<IWebBid>) => {
const { url, password, username, origin_url, active } = removeFalsyValues(bid, ['active']);
const {
url,
password,
username,
origin_url,
active,
arrival_offset_seconds,
// early_login_seconds
} = removeFalsyValues(bid, ["active"]);
try {
const { data } = await axios({
url: `${BASE_URL}/` + bid.id,
withCredentials: true,
method: 'PUT',
data: { url, password, username, origin_url, active },
});
try {
const { data } = await axios({
url: `${BASE_URL}/` + bid.id,
withCredentials: true,
method: "PUT",
data: {
url,
password,
username,
origin_url,
active,
arrival_offset_seconds,
// early_login_seconds
},
});
handleSuccess(data);
handleSuccess(data);
return data;
} catch (error) {
handleError(error);
}
return data;
} catch (error) {
handleError(error);
}
};
export const deleteWebBid = async (web: IWebBid) => {
try {
const { data } = await axios({
url: `${BASE_URL}/` + web.id,
withCredentials: true,
method: 'DELETE',
});
try {
const { data } = await axios({
url: `${BASE_URL}/` + web.id,
withCredentials: true,
method: "DELETE",
});
handleSuccess(data);
handleSuccess(data);
return data;
} catch (error) {
handleError(error);
}
return data;
} catch (error) {
handleError(error);
}
};
export const deletesWebBid = async (web: IWebBid[]) => {
const ids = web.reduce((prev, cur) => {
prev.push(cur.id);
return prev;
}, [] as number[]);
try {
const { data } = await axios({
url: `${BASE_URL}/deletes`,
withCredentials: true,
method: 'POST',
data: {
ids,
},
});
const ids = web.reduce((prev, cur) => {
prev.push(cur.id);
return prev;
}, [] as number[]);
try {
const { data } = await axios({
url: `${BASE_URL}/deletes`,
withCredentials: true,
method: "POST",
data: {
ids,
},
});
handleSuccess(data);
handleSuccess(data);
return data;
} catch (error) {
handleError(error);
}
return data;
} catch (error) {
handleError(error);
}
};

View File

@ -20,7 +20,11 @@ export default function ShowHistoriesBidPicklesApiModal({ data, onUpdated, ...pr
<Table.Tr key={index}>
<Table.Td>{element['bidderAnonName']}</Table.Td>
<Table.Td>{element['actualBid']}</Table.Td>
<<<<<<< HEAD
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString())}</Table.Td>
=======
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
>>>>>>> 26b10a7 (pickxel and fix login)
</Table.Tr>
));
}, [histories]);

View File

@ -13,7 +13,7 @@ export default function ShowHistoriesModal({ data, onUpdated, ...props }: IShowH
<Table.Tr key={element.id}>
<Table.Td>{element.id}</Table.Td>
<Table.Td>{element.price}</Table.Td>
<Table.Td>{formatTime(element.created_at, 'DD/MM/YYYY HH:MM')}</Table.Td>
<Table.Td>{formatTime(new Date(element.created_at).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
</Table.Tr>
));

View File

@ -1,118 +1,196 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Button, LoadingOverlay, Modal, ModalProps, TextInput } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
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 {
Button,
LoadingOverlay,
Modal,
ModalProps,
NumberInput,
TextInput,
} from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import _ from "lodash";
import { useEffect, useRef, useState } from "react";
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";
export interface IWebBidModelProps extends ModalProps {
data: IWebBid | null;
onUpdated?: () => void;
data: IWebBid | null;
onUpdated?: () => void;
}
const schema = {
url: z.string({ message: 'Url is required' }).url('Invalid url format'),
url: z.string({ message: "Url is required" }).url("Invalid url format"),
arrival_offset_seconds: z
.number({ message: "Arrival offset seconds is required" })
.refine((val) => val >= 60, {
message: "Arrival offset seconds must be at least 60 seconds (1 minute)",
}),
early_login_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)",
}),
};
export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelProps) {
const form = useForm({
validate: zodResolver(z.object(schema)),
});
export default function WebBidModal({
data,
onUpdated,
...props
}: IWebBidModelProps) {
const form = useForm({
validate: zodResolver(z.object(schema)),
});
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(false);
const prevData = useRef<IWebBid | null>(data);
const prevData = useRef<IWebBid | null>(data);
const { setConfirm } = useConfirmStore();
const { setConfirm } = useConfirmStore();
const handleSubmit = async (values: typeof form.values) => {
if (data) {
setConfirm({
title: 'Update ?',
message: `This web will be update`,
handleOk: async () => {
setLoading(true);
const result = await updateWebBid(values);
setLoading(false);
const handleSubmit = async (values: typeof form.values) => {
if (data) {
setConfirm({
title: "Update ?",
message: `This web will be update`,
handleOk: async () => {
setLoading(true);
console.log(
"%csrc/components/web-bid/web-bid-modal.tsx:54 values",
"color: #007acc;",
values
);
const result = await updateWebBid(values);
setLoading(false);
if (!result) return;
if (!result) return;
props.onClose();
props.onClose();
if (onUpdated) {
onUpdated();
}
},
okButton: {
color: 'blue',
value: 'Update',
},
});
} else {
const { url, origin_url } = values;
if (onUpdated) {
onUpdated();
}
},
okButton: {
color: "blue",
value: "Update",
},
});
} else {
const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values;
setLoading(true);
const result = await createWebBid({ url, origin_url } as IWebBid);
setLoading(false);
setLoading(true);
const result = await createWebBid({
url,
origin_url,
arrival_offset_seconds,
early_login_seconds
} as IWebBid);
setLoading(false);
if (!result) return;
if (!result) return;
props.onClose();
props.onClose();
if (onUpdated) {
onUpdated();
}
}
};
if (onUpdated) {
onUpdated();
}
}
};
useEffect(() => {
form.reset();
if (!data) return;
useEffect(() => {
form.reset();
if (!data) return;
form.setValues(data);
form.setValues(data);
prevData.current = data;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
prevData.current = data;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
useEffect(() => {
if (!props.opened) {
form.reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.opened]);
useEffect(() => {
if (!props.opened) {
form.reset();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.opened]);
useEffect(() => {
if (form.values?.url) {
form.setFieldValue('origin_url', extractDomain(form.values.url));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.values]);
useEffect(() => {
if (form.values?.url) {
form.setFieldValue("origin_url", extractDomain(form.values.url));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.values]);
return (
<Modal
className="relative"
classNames={{
header: '!flex !item-center !justify-center w-full',
}}
{...props}
size={'xl'}
title={<span className="text-xl font-bold">Web</span>}
centered
return (
<Modal
className="relative"
classNames={{
header: "!flex !item-center !justify-center w-full",
}}
{...props}
size={"xl"}
title={<span className="text-xl font-bold">Web</span>}
centered
>
<form
onSubmit={form.onSubmit(handleSubmit)}
className="grid grid-cols-2 gap-2.5"
>
<TextInput
withAsterisk
className="col-span-2"
size="sm"
label="Domain"
{...form.getInputProps("origin_url")}
/>
<TextInput
withAsterisk
className="col-span-2"
size="sm"
label="Tracking url"
{...form.getInputProps("url")}
/>
<NumberInput
description="Note: that only integer minutes are accepted."
className="col-span-2"
size="sm"
label={`Arrival offset seconds (${
form.getValues()["arrival_offset_seconds"] / 60
} minutes)`}
placeholder="msg: 300"
{...form.getInputProps("arrival_offset_seconds")}
/>
{/* <NumberInput
description="Note: that only integer minutes are accepted."
className="col-span-2"
size="sm"
label={`Early login seconds (${
form.getValues()["early_login_seconds"] / 60
} minutes)`}
placeholder="msg: 600"
{...form.getInputProps("early_login_seconds")}
/> */}
<Button
disabled={_.isEqual(form.getValues(), prevData.current)}
className="col-span-2"
type="submit"
fullWidth
size="sm"
mt="md"
>
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
<TextInput withAsterisk className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
<TextInput withAsterisk className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
{data ? "Update" : "Create"}
</Button>
</form>
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
{data ? 'Update' : 'Create'}
</Button>
</form>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
<LoadingOverlay
visible={loading}
zIndex={1000}
overlayProps={{ blur: 2 }}
/>
</Modal>
);
}

View File

@ -18,24 +18,7 @@ export interface ITimestamp {
updated_at: string;
}
export interface IBid extends ITimestamp {
id: number;
max_price: number;
reserve_price: number;
current_price: number;
name: string | null;
quantity: number;
url: string;
model: string;
lot_id: string;
plus_price: number;
close_time: string | null;
start_bid_time: string | null;
first_bid: boolean;
status: 'biding' | 'out-bid' | 'win-bid';
histories: IHistory[];
web_bid: IWebBid;
}
export interface IHistory extends ITimestamp {
id: number;
@ -59,9 +42,30 @@ export interface IWebBid extends ITimestamp {
username: string | null;
password: string | null;
active: boolean;
arrival_offset_seconds: number;
early_login_seconds: number;
children: IBid[];
}
export interface IBid extends ITimestamp {
id: number;
max_price: number;
reserve_price: number;
current_price: number;
name: string | null;
quantity: number;
url: string;
model: string;
lot_id: string;
plus_price: number;
close_time: string | null;
start_bid_time: string | null;
first_bid: boolean;
status: 'biding' | 'out-bid' | 'win-bid';
histories: IHistory[];
web_bid: IWebBid;
}
export interface IPermission extends ITimestamp {
id: number;
name: string;

View File

@ -3,6 +3,10 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import moment from "moment";
<<<<<<< HEAD
=======
import { IWebBid } from "../system/type";
>>>>>>> 26b10a7 (pickxel and fix login)
export function cn(...args: ClassValue[]) {
return twMerge(clsx(args));
}
@ -150,4 +154,31 @@ export function stringToColor(str: string): string {
const hash = hashStringToInt(str);
const index = hash % colorPalette.length;
return colorPalette[index];
<<<<<<< HEAD
=======
}
export function findEarlyLoginTime(webBid: IWebBid): string | null {
const now = new Date();
// Bước 1: Lọc ra những bid có close_time hợp lệ
const validChildren = webBid.children.filter(child => child.close_time);
if (validChildren.length === 0) return null;
// Bước 2: Tìm bid có close_time gần hiện tại nhất
const closestBid = validChildren.reduce((closest, current) => {
const closestDiff = Math.abs(new Date(closest.close_time!).getTime() - now.getTime());
const currentDiff = Math.abs(new Date(current.close_time!).getTime() - now.getTime());
return currentDiff < closestDiff ? current : closest;
});
if (!closestBid.close_time) return null;
// Bước 3: Tính toán thời gian login sớm
const closeTime = new Date(closestBid.close_time);
closeTime.setSeconds(closeTime.getSeconds() - (webBid.early_login_seconds || 0));
return closeTime.toISOString();
>>>>>>> 26b10a7 (pickxel and fix login)
}

View File

@ -1 +1,5 @@
{"createdAt":1745827424853}
<<<<<<< HEAD
{"createdAt":1745827424853}
=======
{"createdAt":1746413672600}
>>>>>>> 26b10a7 (pickxel and fix login)

View File

@ -21,6 +21,10 @@ export class GraysApi {
switch(bid.web_bid.origin_url){
<<<<<<< HEAD
=======
// GRAYS
>>>>>>> 26b10a7 (pickxel and fix login)
case 'https://www.grays.com': {
const response = await axios({
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}&currencyCode=AUD`,
@ -32,6 +36,11 @@ export class GraysApi {
return AppResponse.toResponse([])
}
<<<<<<< HEAD
=======
// PICKLES
>>>>>>> 26b10a7 (pickxel and fix login)
case 'https://www.pickles.com.au': {
const response = await axios({
@ -39,7 +48,11 @@ export class GraysApi {
});
if (response.data) {
<<<<<<< HEAD
return AppResponse.toResponse(response.data);
=======
return AppResponse.toResponse(response.data.Bids);
>>>>>>> 26b10a7 (pickxel and fix login)
}
return AppResponse.toResponse([])

View File

@ -1,4 +1,4 @@
import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator';
import { IsBoolean, IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator';
export class UpdateWebBidDto {
@IsUrl()
@ -9,6 +9,16 @@ export class UpdateWebBidDto {
@IsOptional()
url: string;
@IsNumber()
@Min(60)
@IsOptional()
arrival_offset_seconds: number;
@IsNumber()
@Min(600)
@IsOptional()
early_login_seconds: number;
@IsString()
@IsOptional()
username: string;

View File

@ -17,6 +17,12 @@ export class WebBid extends Timestamp {
@Column({ default: null, nullable: true })
username: string;
@Column({ default: 300 })
arrival_offset_seconds: number;
@Column({ default: 600 })
early_login_seconds: number;
@Column({ default: null, nullable: true })
@Exclude()
password: string;

View File

@ -228,7 +228,7 @@ 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, 5);
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)

View File

@ -80,7 +80,7 @@ export class WebBidsService {
async emitAccountUpdate(id: WebBid['id']) {
const data = await this.webBidRepo.findOne({
where: { id, children: { status: 'biding' } },
relations: { children: true },
relations: { children: { web_bid: true } },
});
this.eventEmitter.emit(Event.WEB_UPDATED, data || null);

View File

@ -10,7 +10,12 @@ import {
} from "./service/app-service.js";
import browser from "./system/browser.js";
import configs from "./system/config.js";
import { delay, isTimeReached, safeClosePage } from "./system/utils.js";
import {
delay,
findEarlyLoginTime,
isTimeReached,
safeClosePage,
} from "./system/utils.js";
import { updateLoginStatus } from "./system/apis/bid.js";
global.IS_CLEANING = true;
@ -53,6 +58,59 @@ const handleUpdateProductTabs = (data) => {
MANAGER_BIDS = newDataManager;
};
const addProductTab = (data) => {
if (
typeof data !== "object" ||
data === null ||
!Array.isArray(data.children)
) {
console.warn("Data must be an object with a children array");
return;
}
const { children, ...web } = data;
if (children.length === 0) {
console.warn(
`⚠️ No children found for bid id ${web.id}, skipping addProductTab`
);
return;
}
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
const prevApiBid = managerBidMap.get(web.id);
if (prevApiBid) {
// Cập nhật
const updatedChildren = prevApiBid.children;
children.forEach((newChild) => {
const existingChildIndex = updatedChildren.findIndex(
(c) => c.id === newChild.id
);
if (existingChildIndex !== -1) {
updatedChildren[existingChildIndex].setNewData(newChild);
} else {
updatedChildren.push(createBidProduct(web, newChild));
}
});
prevApiBid.setNewData({ ...web, children: updatedChildren });
} else {
// Tạo mới
const newChildren = children.map((item) => createBidProduct(web, item));
const newApiBid = createApiBid({ ...web, children: newChildren });
MANAGER_BIDS.push(newApiBid);
console.log("%cindex.js:116 {MANAGER_BIDS}", "color: #007acc;", {
MANAGER_BIDS,
children: newChildren,
});
}
};
const tracking = async () => {
console.log("🚀 Tracking process started...");
@ -68,6 +126,20 @@ const tracking = async () => {
})
);
await Promise.allSettled(
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
console.log(`🎧 Listening to events for close login: ${apiBid.id}`);
return (apiBid.onCloseLogin = (data) => {
// Loại bỏ class hiện có. Tạo tiền đề cho việc tạo đối tượng mới lại
MANAGER_BIDS = MANAGER_BIDS.filter(
(item) => item.id !== data.id && item.type !== data.type
);
addProductTab(data);
});
})
);
Promise.allSettled(
productTabs.map(async (productTab) => {
console.log(`📌 Processing Product ID: ${productTab.id}`);
@ -159,6 +231,72 @@ const tracking = async () => {
}
};
// const clearLazyTab = async () => {
// if (!global.IS_CLEANING) {
// console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
// return;
// }
// if (!browser) {
// console.warn("⚠️ Browser is not available or disconnected.");
// return;
// }
// try {
// const pages = await browser.pages();
// // Lấy danh sách URL từ flattenedArray
// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
// item.url,
// ...item.children.map((child) => child.url),
// ]).filter(Boolean); // Lọc bỏ null hoặc undefined
// console.log(
// "🔍 Page URLs:",
// pages.map((page) => page.url())
// );
// for (const page of pages) {
// const pageUrl = page.url();
// if (!pageUrl || pageUrl === "about:blank") continue;
// if (!activeUrls.includes(pageUrl)) {
// if (!page.isClosed() && browser.isConnected()) {
// try {
// const bidData = MANAGER_BIDS.filter((item) => item.page_context)
// .map((i) => ({
// current_url: i.page_context.url(),
// data: i,
// }))
// .find((j) => j.current_url === pageUrl);
// console.log(bidData);
// if (bidData && bidData.data) {
// await safeClosePage(bidData.data);
// } else {
// // 👇 Wrap close with timeout + error catch
// await Promise.race([
// page.close(),
// new Promise((_, reject) =>
// setTimeout(() => reject(new Error("Close timeout")), 3000)
// ),
// ]);
// }
// console.log(`🛑 Closing unused tab: ${pageUrl}`);
// } catch (err) {
// console.warn(`⚠️ Error closing tab ${pageUrl}: ${err.message}`);
// }
// }
// }
// }
// } catch (err) {
// console.error("❌ Error in clearLazyTab:", err.message);
// }
// };
const clearLazyTab = async () => {
if (!global.IS_CLEANING) {
console.log("🚀 Cleaning flag is OFF. Proceeding with operation.");
@ -172,47 +310,53 @@ const clearLazyTab = async () => {
try {
const pages = await browser.pages();
console.log("🔍 Found pages:", pages.length);
// Lấy danh sách URL từ flattenedArray
const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [
item.url,
...item.children.map((child) => child.url),
]).filter(Boolean); // Lọc bỏ null hoặc undefined
console.log(
"🔍 Page URLs:",
pages.map((page) => page.url())
);
]).filter(Boolean);
for (const page of pages) {
const pageUrl = page.url();
try {
if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
if (!pageUrl || pageUrl === "about:blank") continue;
const pageUrl = page.url();
if (!activeUrls.includes(pageUrl)) {
if (!page.isClosed() && browser.isConnected()) {
if (!pageUrl || pageUrl === "about:blank") continue;
if (activeUrls.includes(pageUrl)) continue;
page.removeAllListeners();
console.log(`🛑 Unused page detected: ${pageUrl}`);
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
.map((i) => ({
current_url: i.page_context.url(),
data: i,
}))
.find((j) => j.current_url === pageUrl);
if (bidData && bidData.data) {
await safeClosePage(bidData.data);
} else {
try {
const bidData = MANAGER_BIDS.filter((item) => item.page_context)
.map((i) => ({
current_url: i.page_context.url(),
data: i,
}))
.find((j) => j.current_url === pageUrl);
console.log(bidData);
if (bidData && bidData.data) {
await safeClosePage(bidData.data);
} else {
await page.close();
}
console.log(`🛑 Closing unused tab: ${pageUrl}`);
} catch (err) {
console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`);
await Promise.race([
page.close(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Close timeout")), 3000)
),
]);
} catch (closeErr) {
console.warn(
`⚠️ Error closing page ${pageUrl}: ${closeErr.message}`
);
}
}
console.log(`✅ Closed page: ${pageUrl}`);
} catch (pageErr) {
console.warn(`⚠️ Error handling page: ${pageErr.message}`);
}
}
} catch (err) {
@ -343,6 +487,10 @@ const trackingLoginStatus = async () => {
await safeClosePage(tab);
MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id);
addProductTab(data);
global.IS_CLEANING = true;
} else {
console.log("⚠️ No profile found to delete.");

View File

@ -1,106 +1,144 @@
import * as fs from 'fs';
import path from 'path';
import BID_TYPE from '../system/bid-type.js';
import browser from '../system/browser.js';
import CONSTANTS from '../system/constants.js';
import { getPathProfile, sanitizeFileName } from '../system/utils.js';
import { Bid } from './bid.js';
import * as fs from "fs";
import path from "path";
import BID_TYPE from "../system/bid-type.js";
import browser from "../system/browser.js";
import CONSTANTS from "../system/constants.js";
import {
findEarlyLoginTime,
getPathProfile,
isTimeReached,
sanitizeFileName,
} from "../system/utils.js";
import { Bid } from "./bid.js";
export class ApiBid extends Bid {
id;
account;
children = [];
children_processing = [];
created_at;
updated_at;
origin_url;
active;
browser_context;
username;
password;
id;
account;
children = [];
children_processing = [];
created_at;
updated_at;
origin_url;
active;
browser_context;
username;
password;
constructor({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
super(BID_TYPE.API_BID, url);
constructor({
url,
username,
password,
id,
children,
created_at,
updated_at,
origin_url,
active,
}) {
super(BID_TYPE.API_BID, url);
this.created_at = created_at;
this.updated_at = updated_at;
this.children = children;
this.origin_url = origin_url;
this.active = active;
this.username = username;
this.password = password;
this.id = id;
this.created_at = created_at;
this.updated_at = updated_at;
this.children = children;
this.origin_url = origin_url;
this.active = active;
this.username = username;
this.password = password;
this.id = id;
}
setNewData({
url,
username,
password,
id,
children,
created_at,
updated_at,
origin_url,
active,
}) {
this.created_at = created_at;
this.updated_at = updated_at;
this.children = children;
this.origin_url = origin_url;
this.active = active;
this.username = username;
this.password = password;
this.url = url;
}
puppeteer_connect = async () => {
this.browser_context = await browser.createBrowserContext();
const page = await this.browser_context.newPage();
this.page_context = page;
await this.restoreContext();
};
listen_events = async () => {
if (this.page_context) return;
await this.puppeteer_connect();
await this.action();
await this.saveContext();
};
async saveContext() {
if (!this.browser_context || !this.page_context) return;
try {
const cookies = await this.browser_context.cookies();
const localStorageData = await this.page_context.evaluate(() =>
JSON.stringify(localStorage)
);
const contextData = {
cookies,
localStorage: localStorageData,
};
const dirPath = path.join(CONSTANTS.PROFILE_PATH);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
console.log(`📂 Save at folder: ${dirPath}`);
}
fs.writeFileSync(
path.join(dirPath, sanitizeFileName(this.origin_url) + ".json"),
JSON.stringify(contextData, null, 2)
);
console.log("✅ Context saved!");
} catch (error) {
console.log("Save Context: ", error.message);
}
}
setNewData({ url, username, password, id, children, created_at, updated_at, origin_url, active }) {
this.created_at = created_at;
this.updated_at = updated_at;
this.children = children;
this.origin_url = origin_url;
this.active = active;
this.username = username;
this.password = password;
this.url = url;
}
async restoreContext() {
if (!this.browser_context || !this.page_context) return;
puppeteer_connect = async () => {
this.browser_context = await browser.createBrowserContext();
const filePath = getPathProfile(this.origin_url);
const page = await this.browser_context.newPage();
if (!fs.existsSync(filePath)) return;
this.page_context = page;
const contextData = JSON.parse(fs.readFileSync(filePath, "utf8"));
await this.restoreContext();
};
// Restore Cookies
await this.page_context.setCookie(...contextData.cookies);
listen_events = async () => {
if (this.page_context) return;
console.log("🔄 Context restored!");
}
await this.puppeteer_connect();
async onCloseLogin() {}
await this.action();
async isTimeToLogin() {
const earlyLoginTime = findEarlyLoginTime(this);
await this.saveContext();
};
async saveContext() {
if (!this.browser_context || !this.page_context) return;
try {
const cookies = await this.browser_context.cookies();
const localStorageData = await this.page_context.evaluate(() => JSON.stringify(localStorage));
const contextData = {
cookies,
localStorage: localStorageData,
};
const dirPath = path.join(CONSTANTS.PROFILE_PATH);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
console.log(`📂 Save at folder: ${dirPath}`);
}
fs.writeFileSync(path.join(dirPath, sanitizeFileName(this.origin_url) + '.json'), JSON.stringify(contextData, null, 2));
console.log('✅ Context saved!');
} catch (error) {
console.log('Save Context: ', error.message);
}
}
async restoreContext() {
if (!this.browser_context || !this.page_context) return;
const filePath = getPathProfile(this.origin_url);
if (!fs.existsSync(filePath)) return;
const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
// Restore Cookies
await this.page_context.setCookie(...contextData.cookies);
console.log('🔄 Context restored!');
}
return earlyLoginTime && isTimeReached(earlyLoginTime);
}
}

View File

@ -85,6 +85,8 @@ export class LangtonsApiBid extends ApiBid {
`➡ [${this.id}] Closing main page context: ${this.page_context}`
);
await safeClosePage(this);
await this.onCloseLogin(this);
}
console.log(`🔑 [${this.id}] Starting login process...`);

View File

@ -79,9 +79,21 @@ export class LangtonsProductBid extends ProductBid {
timeout / 1000
}s`
);
this.page_context.off("response", onResponse); // Gỡ bỏ listener khi timeout
this.page_context?.off("response", onResponse); // Gỡ bỏ listener khi timeout
await this.page_context.reload({ waitUntil: "networkidle0" }); // reload page
try {
if (!this.page_context.isClosed()) {
await this.page_context.reload({ waitUntil: "networkidle0" });
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
} else {
console.log(`⚠️ [${this.id}] Cannot reload, page already closed.`);
}
} catch (error) {
console.error(
`❌ [${this.id}] Error reloading page:`,
error?.message
);
}
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
resolve(null);
@ -161,8 +173,8 @@ export class LangtonsProductBid extends ProductBid {
lot_id: result?.lotId || null,
reserve_price: result.lotData?.minimumBid || null,
current_price: result.lotData?.currentMaxBid || null,
// close_time: close_time && !this.close_time ? String(close_time) : null,
close_time: close_time ? String(close_time) : null,
// close_time: close_time && !this.close_time ? String(close_time) : null,
name,
},
// [],
@ -441,7 +453,11 @@ export class LangtonsProductBid extends ProductBid {
console.log(
`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`
);
await this.page_context.reload({ waitUntil: "networkidle0" });
if (!this.page_context.isClosed()) {
await this.page_context.reload({ waitUntil: "networkidle0" });
}
console.log(`🔁 [${this.id}] Reload page in gotoLink`);
return;
}

View File

@ -248,7 +248,7 @@ export class LawsonsProductBid extends ProductBid {
`===============Start call to submit [${this.id}] ================`
);
await delay(2000);
await delay(200);
// Nếu chưa bid, thực hiện đặt giá
console.log(
@ -368,7 +368,10 @@ export class LawsonsProductBid extends ProductBid {
console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
}
if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) {
if (
new Date(this.updated_at).getTime() > Date.now() - 120 * 1000 &&
!this.page_context.isClosed()
) {
await this.page_context.reload({ waitUntil: "networkidle0" });
}
} catch (error) {

View File

@ -1,108 +1,147 @@
import CONSTANTS from './constants.js';
import fs from 'fs';
import path from 'path';
import { updateStatusWork } from './apis/bid.js';
import CONSTANTS from "./constants.js";
import fs from "fs";
import path from "path";
import { updateStatusWork } from "./apis/bid.js";
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
if (!page || page.isClosed()) return;
export const takeSnapshot = async (
page,
item,
imageName,
type = CONSTANTS.TYPE_IMAGE.ERRORS
) => {
if (!page || page.isClosed()) return;
try {
const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id
const typeDir = path.join(baseDir, type); // Thư mục con theo type
try {
const baseDir = path.join(
CONSTANTS.ERROR_IMAGES_PATH,
item.type,
String(item.id)
); // Thư mục theo lot_id
const typeDir = path.join(baseDir, type); // Thư mục con theo type
// Tạo tên file, nếu type === 'work' thì không có timestamp
const fileName = type === CONSTANTS.TYPE_IMAGE.WORK ? `${imageName}.png` : `${imageName}_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
// Tạo tên file, nếu type === 'work' thì không có timestamp
const fileName =
type === CONSTANTS.TYPE_IMAGE.WORK
? `${imageName}.png`
: `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
const filePath = path.join(typeDir, fileName);
const filePath = path.join(typeDir, fileName);
// Kiểm tra và tạo thư mục nếu chưa tồn tại
if (!fs.existsSync(typeDir)) {
fs.mkdirSync(typeDir, { recursive: true });
console.log(`📂 Save at folder: ${typeDir}`);
}
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
// Kiểm tra có thể điều hướng trang không
const isPageResponsive = await page.evaluate(() => document.readyState === 'complete');
if (!isPageResponsive) {
console.log('🚫 Page is unresponsive, skipping snapshot.');
return;
}
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => {
console.log('⚠️ Body selector not found, skipping snapshot.');
return;
});
// Chụp ảnh màn hình và lưu vào filePath
await page.screenshot({ path: filePath });
console.log(`📸 Image saved at: ${filePath}`);
// Nếu type === 'work', gửi ảnh lên API
if (type === CONSTANTS.TYPE_IMAGE.WORK) {
await updateStatusWork(item, filePath);
}
} catch (error) {
console.log('Error when snapshot: ' + error.message);
// Kiểm tra và tạo thư mục nếu chưa tồn tại
if (!fs.existsSync(typeDir)) {
fs.mkdirSync(typeDir, { recursive: true });
console.log(`📂 Save at folder: ${typeDir}`);
}
// await page.waitForSelector('body', { visible: true, timeout: 5000 });
// Kiểm tra có thể điều hướng trang không
const isPageResponsive = await page.evaluate(
() => document.readyState === "complete"
);
if (!isPageResponsive) {
console.log("🚫 Page is unresponsive, skipping snapshot.");
return;
}
// Chờ tối đa 15 giây, nếu không thấy thì bỏ qua
await page
.waitForSelector("body", { visible: true, timeout: 15000 })
.catch(() => {
console.log("⚠️ Body selector not found, skipping snapshot.");
return;
});
// Chụp ảnh màn hình và lưu vào filePath
await page.screenshot({ path: filePath });
console.log(`📸 Image saved at: ${filePath}`);
// Nếu type === 'work', gửi ảnh lên API
if (type === CONSTANTS.TYPE_IMAGE.WORK) {
await updateStatusWork(item, filePath);
}
} catch (error) {
console.log("Error when snapshot: " + error.message);
}
};
export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function safeClosePage(item) {
try {
const page = item.page_context;
export const safeClosePageReal = async (page) => {
if (!page) return;
if (!page?.isClosed() && page?.close) {
await page.close();
}
item.page_context = undefined;
if (item?.page_context) {
item.page_context = undefined;
}
} catch (error) {
console.log("Can't close item: " + item.id);
try {
if (page.isClosed()) {
console.log(`✅ Page already closed: ${page.url()}`);
return;
}
page.removeAllListeners(); // ✂️ Remove hết listeners trước khi close
await page.close({ runBeforeUnload: true }); // 🛑 Đóng an toàn
console.log(`✅ Successfully closed page: ${page.url()}`);
} catch (err) {
console.warn(
`⚠️ Error closing page ${page.url ? page.url() : ""}: ${err.message}`
);
}
};
export async function safeClosePage(item) {
try {
const page = item.page_context;
if (!page?.isClosed() && page?.close) {
await safeClosePageReal(page);
// await page.close();
}
item.page_context = undefined;
if (item?.page_context) {
item.page_context = undefined;
}
} catch (error) {
console.log("Can't close item: " + item.id);
}
}
export function isTimeReached(targetTime) {
if (!targetTime) return false;
if (!targetTime) return false;
const targetDate = new Date(targetTime);
const now = new Date();
const targetDate = new Date(targetTime);
const now = new Date();
return now >= targetDate;
return now >= targetDate;
}
export function extractNumber(str) {
const match = str.match(/\d+(\.\d+)?/);
return match ? parseFloat(match[0]) : null;
const match = str.match(/\d+(\.\d+)?/);
return match ? parseFloat(match[0]) : null;
}
export const sanitizeFileName = (url) => {
return url.replace(/[:\/]/g, '_');
return url.replace(/[:\/]/g, "_");
};
export const getPathProfile = (origin_url) => {
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
return path.join(
CONSTANTS.PROFILE_PATH,
sanitizeFileName(origin_url) + ".json"
);
};
export function removeFalsyValues(obj, excludeKeys = []) {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key)) {
acc[key] = value;
}
return acc;
}, {});
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key)) {
acc[key] = value;
}
return acc;
}, {});
}
export const enableAutoBidMessage = (data) => {
return `
return `
<b> Activate Auto Bid</b><br>
📌 Product: <b>${data.name}</b><br>
🔗 Link: <a href="${data.url}">Click here</a><br>
@ -112,69 +151,110 @@ export const enableAutoBidMessage = (data) => {
};
export function convertAETtoUTC(dateString) {
// Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
const monthMap = {
Jan: 0,
Feb: 1,
Mar: 2,
Apr: 3,
May: 4,
Jun: 5,
Jul: 6,
Aug: 7,
Sep: 8,
Oct: 9,
Nov: 10,
Dec: 11,
};
// Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript)
const monthMap = {
Jan: 0,
Feb: 1,
Mar: 2,
Apr: 3,
May: 4,
Jun: 5,
Jul: 6,
Aug: 7,
Sep: 8,
Oct: 9,
Nov: 10,
Dec: 11,
};
// Tách chuỗi đầu vào
const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/);
if (!parts) {
throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
}
// Tách chuỗi đầu vào
const parts = dateString.match(
/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/
);
if (!parts) {
throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'");
}
const [, , day, month, year, hour, period] = parts;
const [, , day, month, year, hour, period] = parts;
// Chuyển đổi giờ sang định dạng 24h
let hours = parseInt(hour, 10);
if (period === 'PM' && hours !== 12) hours += 12;
if (period === 'AM' && hours === 12) hours = 0;
// Chuyển đổi giờ sang định dạng 24h
let hours = parseInt(hour, 10);
if (period === "PM" && hours !== 12) hours += 12;
if (period === "AM" && hours === 12) hours = 0;
// Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
const date = new Date(Date.UTC(parseInt(year, 10), monthMap[month], parseInt(day, 10), hours, 0, 0));
// Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ)
const date = new Date(
Date.UTC(
parseInt(year, 10),
monthMap[month],
parseInt(day, 10),
hours,
0,
0
)
);
// Hàm kiểm tra DST cho AET
function isDST(date) {
const year = date.getUTCFullYear();
const month = date.getUTCMonth();
const day = date.getUTCDate();
// Hàm kiểm tra DST cho AET
function isDST(date) {
const year = date.getUTCFullYear();
const month = date.getUTCMonth();
const day = date.getUTCDate();
// DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT)
const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10
dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên
const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10
// DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT)
const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10
dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên
const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10
// DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST)
const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4
dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên
const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11
// DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST)
const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4
dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên
const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11
const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
return currentTime >= dstStartTime && currentTime < dstEndTime;
}
const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10)
return currentTime >= dstStartTime && currentTime < dstEndTime;
}
// Xác định offset dựa trên DST
const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không
// Xác định offset dựa trên DST
const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không
// Điều chỉnh thời gian về UTC
const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
// Điều chỉnh thời gian về UTC
const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000);
// Trả về chuỗi UTC
return utcDate.toUTCString();
// Trả về chuỗi UTC
return utcDate.toUTCString();
}
export function extractPriceNumber(priceString) {
const cleaned = priceString.replace(/[^\d.]/g, '');
return parseFloat(cleaned);
const cleaned = priceString.replace(/[^\d.]/g, "");
return parseFloat(cleaned);
}
export function findEarlyLoginTime(webBid) {
const now = new Date();
// Bước 1: Lọc ra những bid có close_time hợp lệ
const validChildren = webBid.children.filter((child) => child.close_time);
if (validChildren.length === 0) return null;
// Bước 2: Tìm bid có close_time gần hiện tại nhất
const closestBid = validChildren.reduce((closest, current) => {
const closestDiff = Math.abs(
new Date(closest.close_time).getTime() - now.getTime()
);
const currentDiff = Math.abs(
new Date(current.close_time).getTime() - now.getTime()
);
return currentDiff < closestDiff ? current : closest;
});
if (!closestBid.close_time) return null;
// Bước 3: Tính toán thời gian login sớm
const closeTime = new Date(closestBid.close_time);
closeTime.setSeconds(
closeTime.getSeconds() - (webBid.early_login_seconds || 0)
);
return closeTime.toISOString();
}