This commit is contained in:
Admin 2025-04-28 16:30:24 +07:00
parent 173841c57c
commit b13712a317
45 changed files with 4190 additions and 2513 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -38,6 +38,7 @@
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6",
"uuid": "^11.0.5",
"yet-another-react-lightbox": "^3.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},
@ -1422,7 +1423,7 @@
"version": "19.0.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz",
"integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
@ -7541,6 +7542,29 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"license": "ISC"
},
"node_modules/yet-another-react-lightbox": {
"version": "3.22.0",
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.22.0.tgz",
"integrity": "sha512-yaXmzUraH/Ftsp7eG/E2leQgXhtrG8c1t+jImlSjC2XtZ7XkvjIV2vP/1kl5kxmsBHjck/98W/9Xxempry+2QQ==",
"license": "MIT",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@types/react": "^16 || ^17 || ^18 || ^19",
"@types/react-dom": "^16 || ^17 || ^18 || ^19",
"react": "^16.8.0 || ^17 || ^18 || ^19",
"react-dom": "^16.8.0 || ^17 || ^18 || ^19"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",

View File

@ -40,6 +40,7 @@
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.6",
"uuid": "^11.0.5",
"yet-another-react-lightbox": "^3.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
},

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { generateNestParams, handleError, handleSuccess } from '.';
import axios from '../lib/axios';
import { IAdmin } from '../system/type';
@ -51,7 +52,8 @@ export const grantNewPasswordAdmin = async (admin: Partial<IAdmin>) => {
};
export const createAdmin = async (admin: Omit<IAdmin, 'id' | 'created_at' | 'updated_at' | 'is_system_account'>) => {
const newData = removeFalsyValues(admin);
const {permissions , ...newData} = removeFalsyValues(admin);
try {
const { data } = await axios({

View File

@ -34,3 +34,18 @@ export const shutdownTool = async () => {
handleError(error);
}
};
export const getStatusTool = async () => {
try {
const { data } = await axios({
url: `${BASE_URL}/status-tool`,
withCredentials: true,
method: 'GET',
});
return data;
} catch (error) {
handleError(error);
}
};

View File

@ -9,7 +9,7 @@ export const handleError = (error: unknown) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = (error as AxiosError).response as Record<string, any>;
const data = response.data;
const data = response?.data;
if (response.status === HttpStatusCode.Forbidden) return;

View File

@ -127,14 +127,14 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro
centered
>
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
<TextInput readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
<TextInput size="sm" label="Email" {...form.getInputProps('email')} />
<TextInput className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
<TextInput withAsterisk readOnly={!!data} size="sm" label="Username" {...form.getInputProps('username')} />
<TextInput withAsterisk size="sm" label="Email" {...form.getInputProps('email')} />
<TextInput withAsterisk className="col-span-2" size="sm" label="Fullname" {...form.getInputProps('fullname')} />
{!data && (
<>
<PasswordInput size="sm" label="Password" {...form.getInputProps('password')} />
<PasswordInput size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
<PasswordInput withAsterisk size="sm" label="Password" {...form.getInputProps('password')} />
<PasswordInput withAsterisk size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
</>
)}

View File

@ -87,8 +87,8 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd
centered
>
<form onSubmit={form.onSubmit(handleSubmit)} className="flex flex-col gap-2.5">
<PasswordInput className="col-span-1" size="sm" label="Password" {...form.getInputProps('password')} />
<PasswordInput className="col-span-1" size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
<PasswordInput withAsterisk className="col-span-1" size="sm" label="Password" {...form.getInputProps('password')} />
<PasswordInput withAsterisk className="col-span-1" size="sm" label="Confirm password" {...form.getInputProps('confirmPassword')} />
<Button className="col-span-2" type="submit" fullWidth size="sm" mt="md">
{'Grant'}

View File

@ -1,3 +1,4 @@
export { default as ShowHistoriesModal } from './show-histories-modal';
export { default as ShowHistoriesBidGraysApiModal } from './show-histories-bid-grays-api-modal';
export { default as ShowHistoriesBidPicklesApiModal } from './show-histories-bid-pickles-api-modal';
export { default as BidModal } from './bid-modal';

View File

@ -17,8 +17,8 @@ export default function ShowHistoriesBidGraysApiModal({ data, onUpdated, ...prop
const [loading, setLoading] = useState(false);
const rows = useMemo(() => {
return histories.map((element) => (
<Table.Tr key={element.LotId}>
return histories.map((element, index) => (
<Table.Tr key={index}>
<Table.Td>{`${element['UserInitials']} - ${element['UserShortAddress']}`}</Table.Td>
<Table.Td>{formatTime(new Date(extractNumber(element['OriginalDate']) || 0).toUTCString(), 'HH:mm:ss DD/MM/YYYY')}</Table.Td>
<Table.Td>{`AU $${element['Price']}`}</Table.Td>

View File

@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { LoadingOverlay, Modal, ModalProps, Table } from '@mantine/core';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { getDetailBidHistories } from '../../apis/bid-histories';
import { IBid } from '../../system/type';
import { formatTime } from '../../utils';
export interface IShowHistoriesBidGraysApiModalProps extends ModalProps {
data: IBid | null;
onUpdated?: () => void;
}
export default function ShowHistoriesBidPicklesApiModal({ data, onUpdated, ...props }: IShowHistoriesBidGraysApiModalProps) {
const [histories, setHistories] = useState<Record<string, string>[]>([]);
const [loading, setLoading] = useState(false);
const rows = useMemo(() => {
return histories.map((element, index) => (
<Table.Tr key={index}>
<Table.Td>{element['bidderAnonName']}</Table.Td>
<Table.Td>{element['actualBid']}</Table.Td>
<Table.Td>{formatTime(new Date(element['bidTimeInMilliSeconds']).toUTCString())}</Table.Td>
</Table.Tr>
));
}, [histories]);
const handleCallApi = useCallback(async () => {
if (!data?.lot_id) {
setHistories([]);
return;
}
setLoading(true);
const response = await getDetailBidHistories(data?.lot_id);
setLoading(false);
if (response.data) {
setHistories(response.data);
}
}, [data]);
useEffect(() => {
handleCallApi();
}, [handleCallApi]);
return (
<Modal className="relative" {...props} size="xl" title={<span className="text-xl font-bold">BIDDING HISTORY</span>} centered>
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr>
<Table.Th>Bidder name</Table.Th>
<Table.Th>Actual bid</Table.Th>
<Table.Th>Time</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{histories.length <= 0 ? (
<Table.Tr>
<Table.Td colSpan={5} className="text-center">
None
</Table.Td>
</Table.Tr>
) : (
rows
)}
</Table.Tbody>
</Table>
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ blur: 2 }} />
</Modal>
);
}

View File

@ -1,19 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Image, Modal, ModalProps, ScrollArea } from '@mantine/core';
export default function ShowImageModal({ src, fallbackSrc, ...props }: ModalProps & { src: string; fallbackSrc: string }) {
import { ModalProps } from '@mantine/core';
import Lightbox from "yet-another-react-lightbox";
import "yet-another-react-lightbox/plugins/captions.css";
import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen";
import "yet-another-react-lightbox/plugins/thumbnails.css";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import "yet-another-react-lightbox/styles.css";
export default function ShowImageModal({ src, fallbackSrc,opened, onClose, ...props }: ModalProps & { src: string; fallbackSrc: string }) {
return (
<Modal
classNames={{
header: '!flex !item-center !justify-center w-full',
}}
{...props}
size={'xl'}
title={<span className="text-xl font-bold">Image</span>}
centered
scrollAreaComponent={ScrollArea.Autosize}
>
<Image src={src} fallbackSrc={fallbackSrc} />
</Modal>
<Lightbox {...props} open={opened} close={onClose} slides={[{ src: src || fallbackSrc }]}
plugins={[Fullscreen, Zoom]}/>
);
}

View File

@ -1,104 +1,196 @@
import { Box, Button, Image, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import moment from 'moment';
import { useEffect, useState } from 'react';
import { Socket } from 'socket.io-client';
import { getImagesWorking } from '../../apis/bid';
import { IBid, IWebBid } from '../../system/type';
import ShowImageModal from './show-image-modal';
import { Badge, Box, Button, Image, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import moment from "moment";
import { useEffect, useState } from "react";
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, stringToColor } from "../../utils";
import ShowImageModal from "./show-image-modal";
export interface IWorkingPageProps {
data: (IBid | IWebBid) & { type: string };
socket: Socket;
data: (IBid | IWebBid) & { type: string };
socket: Socket;
}
export default function WorkingPage({ data, socket }: IWorkingPageProps) {
const fallbackSrc = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRGh5WFH8TOIfRKxUrIgJZoDCs1yvQ4hIcppw&s';
const fallbackSrc =
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRGh5WFH8TOIfRKxUrIgJZoDCs1yvQ4hIcppw&s";
const [opened, { open, close }] = useDisclosure(false);
const [opened, { open, close }] = useDisclosure(false);
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
function isIBid(obj: IBid | IWebBid): obj is IBid {
return 'name' in obj;
const [payloadLoginStatus, setPayloadLoginStatus] = useState<{
data: IWebBid;
login_status: boolean;
} | null>(null);
const { statusTool } = useStatusToolStore();
function isIBid(obj: IBid | IWebBid): obj is IBid {
return "name" in obj;
}
const renderUrl = (
{ type, id }: (IBid | IWebBid) & { type: string },
name: string
) => {
return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type
.replace("_", "-")
.toLowerCase()}/${id}/${name}`;
};
const extractTime = (filename: string) => {
return Number(filename.split("-")[0]) || 0;
};
const statusLabel = () => {
if (
statusTool &&
statusTool === "online" &&
payloadLoginStatus?.login_status
) {
return "logined";
}
return !statusTool || statusTool !== "online" ? "Unknown" : "logout";
};
const renderUrl = ({ type, id }: (IBid | IWebBid) & { type: string }, name: string) => {
return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${name}`;
useEffect(() => {
const updateImage = ({
type,
id,
filename,
}: {
type: string;
filename: string;
id: IBid["id"];
}) => {
if (type == data.type && id == data.id) {
setLastUpdate(new Date(extractTime(filename)));
setImageSrc(renderUrl(data, filename));
}
};
const extractTime = (filename: string) => {
return Number(filename.split('-')[0]) || 0;
socket.on("working", updateImage);
return () => {
socket.off("working", updateImage);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [socket, data.id, data.type]);
useEffect(() => {
const onLoginStatus = (data: { data: IWebBid; login_status: boolean }) => {
setPayloadLoginStatus(data);
console.log(
"%csrc/components/dashboard/working-page.tsx:60 data",
"color: #007acc;",
data
);
};
useEffect(() => {
const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => {
if (type == data.type && id == data.id) {
setLastUpdate(new Date(extractTime(filename)));
setImageSrc(renderUrl(data, filename));
}
};
const origin_url = isIBid(data) ? data.web_bid.origin_url : data.origin_url;
socket.on('working', updateImage);
socket.on(`login-status.${origin_url}`, onLoginStatus);
return () => {
socket.off('working', updateImage);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [socket, data.id, data.type]);
return () => {
socket.off(`login-status.${origin_url}`, onLoginStatus);
};
}, [data, socket]);
useEffect(() => {
(async () => {
const result = await getImagesWorking(data);
useEffect(() => {
(async () => {
const result = await getImagesWorking(data);
if (!result || !result.data) return;
if (!result || !result.data) return;
const filename = result.data[0];
const filename = result.data[0];
setImageSrc(renderUrl(data, filename));
setLastUpdate(new Date(extractTime(filename)));
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
setImageSrc(renderUrl(data, filename));
setLastUpdate(new Date(extractTime(filename)));
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<Box className="rounded-md overflow-hidden relative shadow-lg">
<Image
radius="md"
h={300}
style={{
objectFit: 'cover',
}}
fallbackSrc={fallbackSrc}
src={imageSrc}
/>
return (
<>
<Box
className={cn("rounded-md overflow-hidden relative shadow-lg", {
["border border-green-800"]: payloadLoginStatus?.login_status,
["border border-red-800"]: !payloadLoginStatus?.login_status,
})}
>
<Image
radius="md"
h={300}
style={{
objectFit: "cover",
}}
fallbackSrc={fallbackSrc}
src={imageSrc}
/>
<Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
<Text className="text-lg tracking-wide text-center font-bold">{isIBid(data) ? data.name : 'Tracking page'}</Text>
{isIBid(data) && <Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>}
{isIBid(data) && <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>
<Box className="flex items-center gap-4">
<Button size="xs" color="green" onClick={open} className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition">
Show
</Button>
<Button
target="_blank"
component="a"
size="xs"
href={data.url || '/'}
className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
>
Link
</Button>
</Box>
</Box>
</Box>
<Box className="absolute bg-black/60 inset-0 flex items-center justify-center text-white font-bold flex-col gap-3 p-4 rounded-lg transition duration-300 hover:bg-opacity-70">
<Text className="text-lg tracking-wide text-center font-bold">
{isIBid(data) ? data.name : "Tracking page"}
</Text>
{isIBid(data) && (
<Text className="text-xs tracking-wide">{`Max price: $${data.max_price}`}</Text>
)}
{isIBid(data) && (
<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>
<Box className="flex items-center gap-4">
<Button
size="xs"
color="green"
onClick={open}
className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
>
Show
</Button>
<Button
target="_blank"
component="a"
size="xs"
href={data.url || "/"}
className="bg-white text-black px-4 py-2 rounded-md shadow-md hover:bg-gray-200 transition"
>
Link
</Button>
</Box>
</Box>
<ShowImageModal src={imageSrc || fallbackSrc} fallbackSrc={fallbackSrc} opened={opened} onClose={close} />
</>
);
<Box className="absolute top-2.5 left-2.5 flex items-center gap-2">
<Badge
color={payloadLoginStatus?.login_status ? "green" : "red"}
size="xs"
>
{statusLabel()}
</Badge>
<Badge
color={stringToColor(isIBid(data) ? data.web_bid.origin_url : data.origin_url)}
size="xs"
>
{isIBid(data) ? data.web_bid.origin_url : data.origin_url}
</Badge>
</Box>
</Box>
<ShowImageModal
src={imageSrc || fallbackSrc}
fallbackSrc={fallbackSrc}
opened={opened}
onClose={close}
/>
</>
);
}

View File

@ -104,8 +104,8 @@ export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelP
centered
>
<form onSubmit={form.onSubmit(handleSubmit)} className="grid grid-cols-2 gap-2.5">
<TextInput className="col-span-2" size="sm" label="Domain" {...form.getInputProps('origin_url')} />
<TextInput className="col-span-2" size="sm" label="Tracking url" {...form.getInputProps('url')} />
<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')} />
<Button disabled={_.isEqual(form.getValues(), prevData.current)} className="col-span-2" type="submit" fullWidth size="sm" mt="md">
{data ? 'Update' : 'Create'}

View File

@ -0,0 +1,8 @@
const constants = {
grays: 'https://www.grays.com',
pickles:'https://www.pickles.com.au'
}
export const haveHistories = [constants.grays, constants.pickles]
export default constants

View File

@ -0,0 +1,13 @@
import { create } from "zustand";
type TStatusToolState = {
statusTool: string | boolean;
setStatusTool: (value: TStatusToolState["statusTool"]) => void;
};
export const useStatusToolStore = create<TStatusToolState>((set) => ({
statusTool: false,
props: {},
setStatusTool: (value: TStatusToolState["statusTool"]) =>
set({ statusTool: value }),
}));

View File

@ -1,340 +1,410 @@
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconAd, IconAdOff, IconEdit, IconHammer, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react';
import _ from 'lodash';
import { useMemo, useRef, useState } from 'react';
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
import { BidModal, ShowHistoriesBidGraysApiModal, ShowHistoriesModal } from '../components/bid';
import Table from '../lib/table/table';
import { IColumn, TRefTableFn } from '../lib/table/type';
import { useConfirmStore } from '../lib/zustand/use-confirm';
import { mappingStatusColors } from '../system/constants';
import { IBid } from '../system/type';
import { formatTime } from '../utils';
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import {
IconAd,
IconAdOff,
IconEdit,
IconHammer,
IconHistory,
IconMenu,
IconTrash,
} from "@tabler/icons-react";
import _ from "lodash";
import { useMemo, useRef, useState } from "react";
import { deleteBid, deletesBid, getBids, toggleBid } from "../apis/bid";
import {
BidModal,
ShowHistoriesBidGraysApiModal,
ShowHistoriesBidPicklesApiModal,
ShowHistoriesModal,
} from "../components/bid";
import Table from "../lib/table/table";
import { IColumn, TRefTableFn } from "../lib/table/type";
import { useConfirmStore } from "../lib/zustand/use-confirm";
import { mappingStatusColors } from "../system/constants";
import { IBid } from "../system/type";
import { formatTime } from "../utils";
import constants, { haveHistories } from "../constant";
export default function Bids() {
const refTableFn: TRefTableFn<IBid> = useRef({});
const refTableFn: TRefTableFn<IBid> = useRef({});
const [clickData, setClickData] = useState<IBid | null>(null);
const [clickData, setClickData] = useState<IBid | null>(null);
const { setConfirm } = useConfirmStore();
const { setConfirm } = useConfirmStore();
const [openedHistories, historiesModel] = useDisclosure(false);
const [openedHistoriesGraysApi, historiesGraysApiModel] = useDisclosure(false);
const [openedBid, bidModal] = useDisclosure(false);
const [openedHistories, historiesModel] = useDisclosure(false);
const [openedHistoriesGraysApi, historiesGraysApiModel] =
useDisclosure(false);
const columns: IColumn<IBid>[] = [
{
key: 'id',
title: 'ID',
typeFilter: 'number',
},
{
key: 'name',
title: 'Name',
typeFilter: 'text',
},
{
key: 'web_bid',
title: 'Web',
typeFilter: 'text',
renderRow(row) {
return <span>{row.web_bid.origin_url}</span>;
},
},
{
key: 'lot_id',
title: 'Lot ID',
typeFilter: 'text',
},
{
key: 'model',
title: 'Model',
typeFilter: 'text',
},
const [openedHistoriesPicklesApi, historiesPicklesApiModel] =
useDisclosure(false);
const [openedBid, bidModal] = useDisclosure(false);
{
key: 'plus_price',
title: 'Plus price',
typeFilter: 'number',
},
{
key: 'max_price',
title: 'Max price',
typeFilter: 'number',
},
{
key: 'current_price',
title: 'Current price',
typeFilter: 'number',
},
{
key: 'reserve_price',
title: 'Reserve price',
typeFilter: 'number',
},
{
key: 'histories',
title: 'Current bid',
typeFilter: 'none',
renderRow(row) {
const bidPrice = _.maxBy(row.histories, 'price');
const columns: IColumn<IBid>[] = [
{
key: "id",
title: "ID",
typeFilter: "number",
},
{
key: "name",
title: "Name",
typeFilter: "text",
},
{
key: "web_bid",
title: "Web",
typeFilter: "text",
renderRow(row) {
return <span>{row.web_bid.origin_url}</span>;
},
},
{
key: "lot_id",
title: "Lot ID",
typeFilter: "text",
},
{
key: "model",
title: "Model",
typeFilter: "text",
},
return <Text>{bidPrice ? bidPrice.price : 'None'}</Text>;
},
},
{
key: 'start_bid_time',
title: 'Start bid',
typeFilter: 'text',
renderRow(row) {
return (
<Tooltip hidden={!row.start_bid_time} label={row.start_bid_time}>
<Text size="sm">{row.start_bid_time ? formatTime(row.start_bid_time, 'HH:mm:ss DD/MM/YYYY') : 'None'}</Text>
</Tooltip>
);
},
},
{
key: 'close_time',
title: 'Close time',
typeFilter: 'text',
renderRow(row) {
return (
<Tooltip hidden={!row.close_time} label={row.close_time}>
<Text size="sm">{row.close_time ? formatTime(row.close_time, 'HH:mm:ss DD/MM/YYYY') : 'None'}</Text>
</Tooltip>
);
},
},
{
key: 'status',
title: 'Status',
typeFilter: 'text',
renderRow(row) {
return (
<Box className="flex items-center justify-center">
<Badge color={mappingStatusColors[row.status]} size="sm">
{row.status}
</Badge>
</Box>
);
},
},
];
{
key: "plus_price",
title: "Plus price",
typeFilter: "number",
},
{
key: "max_price",
title: "Max price",
typeFilter: "number",
},
{
key: "current_price",
title: "Current price",
typeFilter: "number",
},
{
key: "reserve_price",
title: "Reserve price",
typeFilter: "number",
},
{
key: "histories",
title: "Current bid",
typeFilter: "none",
renderRow(row) {
const bidPrice = _.maxBy(row.histories, "price");
const handleDelete = (bid: IBid) => {
setConfirm({
title: 'Delete ?',
message: 'This bid will be delete',
handleOk: async () => {
await deleteBid(bid);
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
},
});
};
const handleToggleBid = async (bid: IBid) => {
const isEnable = bid.status === 'biding' ? true : bid.status === 'out-bid' ? false : true;
setConfirm({
title: (isEnable ? 'Disable ' : 'Enable ') + 'ID: ' + bid.id,
message: 'This bid will be ' + (isEnable ? 'disable ' : 'enable '),
handleOk: async () => {
await toggleBid(bid);
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
},
okButton: {
value: isEnable ? 'Disable ' : 'Enable ',
color: isEnable ? 'red' : 'blue',
},
});
};
const table = useMemo(() => {
return <Text>{bidPrice ? bidPrice.price : "None"}</Text>;
},
},
{
key: "start_bid_time",
title: "Start bid",
typeFilter: "text",
renderRow(row) {
return (
<Table
onClickRow={(row) => {
window.open(row.url, '_blank');
}}
tableChildProps={{
trbody: {
className: 'cursor-pointer',
},
}}
actionsOptions={{
actions: [
{
key: 'add',
title: 'Add',
callback: () => {
bidModal.open();
},
},
{
key: 'delete',
title: 'Delete',
callback: (data) => {
if (!data.length) return;
setConfirm({
title: 'Delete',
message: `${data.length} will be delete`,
handleOk: async () => {
const result = await deletesBid(data);
if (!result) return;
if (refTableFn.current.fetchData) {
refTableFn.current.fetchData();
}
},
});
},
disabled: (data) => data.length <= 0,
},
],
}}
refTableFn={refTableFn}
striped
showLoading={true}
highlightOnHover
styleDefaultHead={{
justifyContent: 'flex-start',
width: 'fit-content',
}}
options={{
query: getBids,
pathToData: 'data.data',
keyOptions: {
last_page: 'lastPage',
per_page: 'perPage',
from: 'from',
to: 'to',
total: 'total',
},
}}
rows={[]}
withColumnBorders
showChooses={true}
withTableBorder
columns={columns}
actions={{
title: <Box className="w-full text-center">Action</Box>,
body: (row) => {
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<Box onClick={(e) => e.stopPropagation()} className="flex w-full items-center justify-center">
<ActionIcon size="sm" variant="light">
<IconMenu size={14} />
</ActionIcon>
</Box>
</Menu.Target>
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
<Menu.Item
onClick={() => {
setClickData(row);
bidModal.open();
}}
leftSection={<IconEdit size={14} />}
>
Edit
</Menu.Item>
<Menu.Item
onClick={() => {
setClickData(row);
historiesModel.open();
}}
leftSection={<IconHistory size={14} />}
>
Histories
</Menu.Item>
{['https://www.grays.com'].includes(row?.web_bid.origin_url) && (
<Menu.Item
onClick={() => {
setClickData(row);
historiesGraysApiModel.open();
}}
leftSection={<IconHammer size={14} />}
>
Bids
</Menu.Item>
)}
<Menu.Item
disabled={row.status === 'win-bid'}
onClick={() => handleToggleBid(row)}
leftSection={row.status === 'biding' ? <IconAdOff size={14} /> : <IconAd size={14} />}
>
{row.status === 'biding' ? 'Disable' : 'Enable'}
</Menu.Item>
<Menu.Item onClick={() => handleDelete(row)} leftSection={<IconTrash color="red" size={14} />}>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
},
}}
rowKey="id"
/>
<Tooltip hidden={!row.start_bid_time} label={row.start_bid_time}>
<Text size="sm">
{row.start_bid_time
? formatTime(row.start_bid_time, "HH:mm:ss DD/MM/YYYY")
: "None"}
</Text>
</Tooltip>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
},
},
{
key: "close_time",
title: "Close time",
typeFilter: "text",
renderRow(row) {
return (
<Tooltip hidden={!row.close_time} label={row.close_time}>
<Text size="sm">
{row.close_time
? formatTime(row.close_time, "HH:mm:ss DD/MM/YYYY")
: "None"}
</Text>
</Tooltip>
);
},
},
{
key: "status",
title: "Status",
typeFilter: "text",
renderRow(row) {
return (
<Box className="flex items-center justify-center">
<Badge color={mappingStatusColors[row.status]} size="sm">
{row.status}
</Badge>
</Box>
);
},
},
];
const handleDelete = (bid: IBid) => {
setConfirm({
title: "Delete ?",
message: "This bid will be delete",
handleOk: async () => {
await deleteBid(bid);
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
},
});
};
const handleToggleBid = async (bid: IBid) => {
const isEnable =
bid.status === "biding" ? true : bid.status === "out-bid" ? false : true;
setConfirm({
title: (isEnable ? "Disable " : "Enable ") + "ID: " + bid.id,
message: "This bid will be " + (isEnable ? "disable " : "enable "),
handleOk: async () => {
await toggleBid(bid);
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
},
okButton: {
value: isEnable ? "Disable " : "Enable ",
color: isEnable ? "red" : "blue",
},
});
};
const table = useMemo(() => {
return (
<Box>
{table}
<Table
onClickRow={(row) => {
window.open(row.url, "_blank");
}}
tableChildProps={{
trbody: {
className: "cursor-pointer",
},
}}
actionsOptions={{
actions: [
{
key: "add",
title: "Add",
callback: () => {
bidModal.open();
},
},
{
key: "delete",
title: "Delete",
callback: (data) => {
if (!data.length) return;
setConfirm({
title: "Delete",
message: `${data.length} will be delete`,
handleOk: async () => {
const result = await deletesBid(data);
<ShowHistoriesModal
opened={openedHistories}
onClose={() => {
historiesModel.close();
setClickData(null);
}}
data={clickData}
/>
<BidModal
onUpdated={() => {
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
if (!result) return;
if (refTableFn.current.fetchData) {
refTableFn.current.fetchData();
}
},
});
},
disabled: (data) => data.length <= 0,
},
],
}}
refTableFn={refTableFn}
striped
showLoading={true}
highlightOnHover
styleDefaultHead={{
justifyContent: "flex-start",
width: "fit-content",
}}
options={{
query: getBids,
pathToData: "data.data",
keyOptions: {
last_page: "lastPage",
per_page: "perPage",
from: "from",
to: "to",
total: "total",
},
}}
rows={[]}
withColumnBorders
showChooses={true}
withTableBorder
columns={columns}
actions={{
title: <Box className="w-full text-center">Action</Box>,
body: (row) => {
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<Box
onClick={(e) => e.stopPropagation()}
className="flex w-full items-center justify-center"
>
<ActionIcon size="sm" variant="light">
<IconMenu size={14} />
</ActionIcon>
</Box>
</Menu.Target>
setClickData(null);
}}
opened={openedBid}
onClose={() => {
bidModal.close();
<Menu.Dropdown onClick={(e) => e.stopPropagation()}>
<Menu.Item
onClick={() => {
setClickData(row);
bidModal.open();
}}
leftSection={<IconEdit size={14} />}
>
Edit
</Menu.Item>
setClickData(null);
}}
data={clickData}
/>
<Menu.Item
onClick={() => {
setClickData(row);
historiesModel.open();
}}
leftSection={<IconHistory size={14} />}
>
Histories
</Menu.Item>
{haveHistories.includes(row?.web_bid.origin_url) && (
<Menu.Item
onClick={() => {
setClickData(row);
switch (row.web_bid.origin_url) {
case constants.grays: {
historiesGraysApiModel.open();
break;
}
case constants.pickles: {
historiesPicklesApiModel.open();
break;
}
default: {
historiesGraysApiModel.open();
}
}
}}
leftSection={<IconHammer size={14} />}
>
Bids
</Menu.Item>
)}
<ShowHistoriesBidGraysApiModal
onUpdated={() => {
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
<Menu.Item
disabled={row.status === "win-bid"}
onClick={() => handleToggleBid(row)}
leftSection={
row.status === "biding" ? (
<IconAdOff size={14} />
) : (
<IconAd size={14} />
)
}
>
{row.status === "biding" ? "Disable" : "Enable"}
</Menu.Item>
setClickData(null);
}}
opened={openedHistoriesGraysApi}
onClose={() => {
historiesGraysApiModel.close();
setClickData(null);
}}
data={clickData}
/>
</Box>
<Menu.Item
onClick={() => handleDelete(row)}
leftSection={<IconTrash color="red" size={14} />}
>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
},
}}
rowKey="id"
/>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Box>
{table}
<ShowHistoriesModal
opened={openedHistories}
onClose={() => {
historiesModel.close();
setClickData(null);
}}
data={clickData}
/>
<BidModal
onUpdated={() => {
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
setClickData(null);
}}
opened={openedBid}
onClose={() => {
bidModal.close();
setClickData(null);
}}
data={clickData}
/>
{/* Grays */}
{openedHistoriesGraysApi && (
<ShowHistoriesBidGraysApiModal
onUpdated={() => {
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
setClickData(null);
}}
opened={openedHistoriesGraysApi}
onClose={() => {
historiesGraysApiModel.close();
setClickData(null);
}}
data={clickData}
/>
)}
{openedHistoriesPicklesApi && (
<ShowHistoriesBidPicklesApiModal
onUpdated={() => {
if (refTableFn.current?.fetchData) {
refTableFn.current.fetchData();
}
setClickData(null);
}}
opened={true}
onClose={() => {
historiesPicklesApiModel.close();
setClickData(null);
}}
data={clickData}
/>
)}
</Box>
);
}

View File

@ -1,132 +1,186 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Box, Button, LoadingOverlay, Text, Title } from '@mantine/core';
import { useEffect, useRef, useState } from 'react';
import io from 'socket.io-client';
import { WorkingPage } from '../components/dashboard';
import { IBid, IWebBid } from '../system/type';
import { checkStatus } from '../apis/auth';
import { IconPower, IconRestore } from '@tabler/icons-react';
import { useConfirmStore } from '../lib/zustand/use-confirm';
import { resetTool, shutdownTool } from '../apis/dashboard';
import {
Box,
Button,
LoadingOverlay,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useEffect, useRef, useState } from "react";
import io from "socket.io-client";
import { WorkingPage } from "../components/dashboard";
import { IBid, IWebBid } from "../system/type";
import { checkStatus } from "../apis/auth";
import { IconPower, IconRestore } from "@tabler/icons-react";
import { useConfirmStore } from "../lib/zustand/use-confirm";
import { getStatusTool, resetTool, shutdownTool } from "../apis/dashboard";
import { cn } from "../utils";
import { useStatusToolStore } from "../lib/zustand/use-status-tool-store";
const socket = io(`${import.meta.env.VITE_SOCKET_URL}/admin-bid-ws`, {
autoConnect: true,
transports: ['websocket'],
autoConnect: true,
transports: ["websocket"],
});
export default function DashBoard() {
const [workingData, setWorkingData] = useState<(IWebBid & { type: string })[] | (IBid & { type: string })[]>([]);
const { setConfirm } = useConfirmStore();
const [workingData, setWorkingData] = useState<
(IWebBid & { type: string })[] | (IBid & { type: string })[]
>([]);
const { setConfirm } = useConfirmStore();
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(false);
const RETRY_CONNECT = useRef(2);
const { setStatusTool, statusTool } = useStatusToolStore();
const RETRY_CONNECT = useRef(2);
useEffect(() => {
socket.connect();
socket.on("connect", () => {
socket.emit("getBidsData");
});
socket.on("disconnect", async () => {
if (RETRY_CONNECT.current > 0) {
await checkStatus();
useEffect(() => {
socket.connect();
socket.on('connect', () => {
socket.emit('getBidsData');
});
RETRY_CONNECT.current--;
return;
}
});
socket.on('disconnect', async () => {
if (RETRY_CONNECT.current > 0) {
await checkStatus();
socket.on("adminBidsUpdated", (data: IWebBid[]) => {
const array = data.reduce((prev, cur) => {
if (cur.children?.length > 0) {
prev = [...prev, ...cur.children];
}
prev.push(cur);
return prev;
}, [] as any[]);
socket.connect();
const newData = array.map((item) => {
if (item.children) {
return {
...item,
type: "API_BID",
};
}
RETRY_CONNECT.current--;
return;
}
});
socket.on('adminBidsUpdated', (data: IWebBid[]) => {
const array = data.reduce((prev, cur) => {
if (cur.children?.length > 0) {
prev = [...prev, ...cur.children];
}
prev.push(cur);
return prev;
}, [] as any[]);
const newData = array.map((item) => {
if (item.children) {
return {
...item,
type: 'API_BID',
};
}
return {
...item,
type: 'PRODUCT_TAB',
};
});
setWorkingData(newData);
});
return () => {
console.log('🔌 Cleanup WebSocket listeners...');
socket.off('adminBidsUpdated');
socket.off('working');
socket.off('connect');
socket.off('disconnect');
socket.disconnect();
return {
...item,
type: "PRODUCT_TAB",
};
}, []);
});
setWorkingData(newData);
});
const handleResetTool = () => {
setConfirm({
handleOk: async () => {
setLoading(true);
await resetTool();
setLoading(false);
},
title: 'Confirm tool reset',
message: 'Are you sure you want to reset this tool? All current processes will be stopped and restarted.',
okButton: { value: 'Ok', color: 'blue' },
});
return () => {
console.log("🔌 Cleanup WebSocket listeners...");
socket.off("adminBidsUpdated");
socket.off("working");
socket.off("connect");
socket.off("disconnect");
socket.disconnect();
};
}, []);
useEffect(() => {
const statusTool = async () => {
const result = await getStatusTool();
if (result?.data) {
setStatusTool(result?.data);
} else {
setStatusTool(false);
}
};
const handleShutdownTool = () => {
setConfirm({
handleOk: async () => {
setLoading(true);
await shutdownTool();
setLoading(false);
},
title: 'Confirm tool shutdown',
message: 'Are you sure you want to shut down this tool? All running processes will be stopped and the tool will go offline.',
okButton: { value: 'Ok', color: 'blue' },
});
const intervalId = setInterval(statusTool, 5000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<Box>
<Box className="flex items-center justify-between">
<Title order={2} mb="md">
Admin Dashboard
</Title>
<Box className="flex gap-2">
<Button onClick={handleResetTool} leftSection={<IconRestore size={16} />} size="xs">
Reset tool
</Button>
<Button onClick={handleShutdownTool} leftSection={<IconPower size={16} />} color="red" size="xs">
Shutdown tool
</Button>
</Box>
</Box>
<Box className="grid grid-cols-4 gap-4">
{workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
const handleResetTool = () => {
setConfirm({
handleOk: async () => {
setLoading(true);
await resetTool();
setLoading(false);
},
title: "Confirm tool reset",
message:
"Are you sure you want to reset this tool? All current processes will be stopped and restarted.",
okButton: { value: "Ok", color: "blue" },
});
};
{workingData.length <= 0 && (
<Box className="flex items-center justify-center col-span-4">
<Text>No Pages</Text>
</Box>
)}
</Box>
const handleShutdownTool = () => {
setConfirm({
handleOk: async () => {
setLoading(true);
await shutdownTool();
setLoading(false);
},
title: "Confirm tool shutdown",
message:
"Are you sure you want to shut down this tool? All running processes will be stopped and the tool will go offline.",
okButton: { value: "Ok", color: "blue" },
});
};
<LoadingOverlay visible={loading} />
</Box>
);
return (
<Box>
<Box className="flex items-center justify-between">
<Title order={2} mb="md">
Admin Dashboard
</Title>
<Tooltip label={typeof statusTool === "string" && statusTool}>
<Box
className={cn("flex gap-2 border py-3 px-4 rounded-md", {
["border-green-800"]: statusTool || statusTool === "online",
["border-red-800"]: !statusTool || statusTool !== "online",
})}
>
<Button
color={statusTool === "online" ? "blue" : "green"}
onClick={handleResetTool}
leftSection={<IconRestore size={16} />}
size="xs"
>
{statusTool === "online" ? "Reset tool" : "Start tool"}
</Button>
<Button
onClick={handleShutdownTool}
leftSection={<IconPower size={16} />}
color="red"
size="xs"
>
Shutdown tool
</Button>
</Box>
</Tooltip>
</Box>
<Box className="grid grid-cols-4 gap-4 mt-5">
{workingData.length > 0 &&
workingData.map((item, index) => (
<WorkingPage socket={socket} data={item} key={item.id + index} />
))}
{workingData.length <= 0 && (
<Box className="flex items-center justify-center col-span-4">
<Text>No Pages</Text>
</Box>
)}
</Box>
<LoadingOverlay visible={loading} />
</Box>
);
}

View File

@ -1,112 +1,153 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import moment from 'moment';
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import moment from "moment";
export function cn(...args: ClassValue[]) {
return twMerge(clsx(args));
return twMerge(clsx(args));
}
export const formatTime = (time: string, patent = 'DD/MM/YYYY') => {
return moment(time).format(patent);
export const formatTime = (time: string, patent = "DD/MM/YYYY") => {
return moment(time).format(patent);
};
export function removeFalsyValues<T extends Record<string, any>>(obj: T, excludeKeys: (keyof T)[] = []): Partial<T> {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key as keyof T)) {
acc[key as keyof T] = value;
}
return acc;
}, {} as Partial<T>);
export function removeFalsyValues<T extends Record<string, any>>(
obj: T,
excludeKeys: (keyof T)[] = []
): Partial<T> {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key as keyof T)) {
acc[key as keyof T] = value;
}
return acc;
}, {} as Partial<T>);
}
export function isValidJSON(str: string): boolean {
if (!str || str.length <= 0) return false;
if (!str || str.length <= 0) return false;
try {
JSON.parse(str);
return true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return false;
}
try {
JSON.parse(str);
return true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return false;
}
}
export function copyToClipboard(text: string, onSuccess?: () => void): void {
if (!navigator.clipboard) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
if (!navigator.clipboard) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand('copy');
if (onSuccess) onSuccess();
} catch (err) {
console.error('Không thể copy nội dung: ', err);
}
document.body.removeChild(textarea);
} else {
navigator.clipboard
.writeText(text)
.then(() => {
if (onSuccess) onSuccess();
})
.catch((err) => console.error('Lỗi khi copy nội dung: ', err));
try {
document.execCommand("copy");
if (onSuccess) onSuccess();
} catch (err) {
console.error("Không thể copy nội dung: ", err);
}
document.body.removeChild(textarea);
} else {
navigator.clipboard
.writeText(text)
.then(() => {
if (onSuccess) onSuccess();
})
.catch((err) => console.error("Lỗi khi copy nội dung: ", err));
}
}
export function base64ToFile(base64String: string, fileName: string): File {
const [header, base64Content] = base64String.split(',');
const [header, base64Content] = base64String.split(",");
const mimeTypeMatch = header.match(/:(.*?);/);
if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
throw new Error('Invalid base64 string');
}
const mimeType = mimeTypeMatch[1];
const mimeTypeMatch = header.match(/:(.*?);/);
if (!mimeTypeMatch || mimeTypeMatch.length < 2) {
throw new Error("Invalid base64 string");
}
const mimeType = mimeTypeMatch[1];
const binaryString = atob(base64Content);
const binaryString = atob(base64Content);
const byteArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i);
}
const byteArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i);
}
return new File([byteArray], fileName, { type: mimeType });
return new File([byteArray], fileName, { type: mimeType });
}
export function toSlug(str: string, maxLength = 60): string {
if (typeof str !== 'string') return ''; // Kiểm tra giá trị đầu vào
if (typeof str !== "string") return ""; // Kiểm tra giá trị đầu vào
// Kiểm tra nếu môi trường hỗ trợ `normalize`
const normalizedStr = str.normalize ? str.normalize('NFD') : str;
// Kiểm tra nếu môi trường hỗ trợ `normalize`
const normalizedStr = str.normalize ? str.normalize("NFD") : str;
return normalizedStr
.replace(/[\u0300-\u036f]/g, '') // Xóa dấu
.replace(/[^a-zA-Z0-9\s-]/g, '') // Chỉ giữ chữ cái, số, khoảng trắng và dấu "-"
.trim() // Xóa khoảng trắng đầu/cuối
.replace(/\s+/g, '-') // Thay khoảng trắng bằng "-"
.replace(/-+/g, '-') // Gộp nhiều dấu "-" thành 1
.toLowerCase() // Chuyển về chữ thường
.slice(0, maxLength) // Giới hạn độ dài
.replace(/^-+|-+$/g, ''); // Xóa "-" đầu/cuối
return normalizedStr
.replace(/[\u0300-\u036f]/g, "") // Xóa dấu
.replace(/[^a-zA-Z0-9\s-]/g, "") // Chỉ giữ chữ cái, số, khoảng trắng và dấu "-"
.trim() // Xóa khoảng trắng đầu/cuối
.replace(/\s+/g, "-") // Thay khoảng trắng bằng "-"
.replace(/-+/g, "-") // Gộp nhiều dấu "-" thành 1
.toLowerCase() // Chuyển về chữ thường
.slice(0, maxLength) // Giới hạn độ dài
.replace(/^-+|-+$/g, ""); // Xóa "-" đầu/cuối
}
export function estimateReadingTimeInSeconds(content: string, wordsPerMinute = 200): number {
if (!content || typeof content !== 'string') return 0;
export function estimateReadingTimeInSeconds(
content: string,
wordsPerMinute = 200
): number {
if (!content || typeof content !== "string") return 0;
const wordCount = content.trim().split(/\s+/).length;
return Math.ceil((wordCount / wordsPerMinute) * 60);
const wordCount = content.trim().split(/\s+/).length;
return Math.ceil((wordCount / wordsPerMinute) * 60);
}
export function extractDomain(url: string): string | null {
try {
const parsedUrl = new URL(url);
return parsedUrl.origin;
} catch (error) {
return null;
}
try {
const parsedUrl = new URL(url);
return parsedUrl.origin;
} catch (error) {
return null;
}
}
// Hash chuỗi thành số nguyên
export function hashStringToInt(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; // convert to 32bit integer
}
return Math.abs(hash);
}
// Biến số thành màu HEX
export function intToHexColor(int: number): string {
const r = (int >> 16) & 0xff;
const g = (int >> 8) & 0xff;
const b = int & 0xff;
return `#${[r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
}
export function stringToColor(str: string): string {
const colorPalette = [
"#FF6B6B",
"#FFD93D",
"#FF9F1C",
"#F76C6C",
"#6BCB77",
"#4ECDC4",
"#F7B801",
"#FF6F91",
"#00C9A7",
];
const hash = hashStringToInt(str);
const index = hash % colorPalette.length;
return colorPalette[index];
}

View File

@ -1 +1 @@
{"createdAt":1744861741554}
{"createdAt":1745827424853}

View File

@ -2,18 +2,53 @@ import { Injectable } from '@nestjs/common';
import axios from 'axios';
import AppResponse from 'src/response/app-response';
import { Bid } from '../entities/bid.entity';
import { BidsService } from '../services/bids.service';
@Injectable()
export class GraysApi {
async getHistoriesBid(lot_id: Bid['lot_id']) {
try {
const response = await axios({
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}&currencyCode=AUD`,
});
if (response.data && response.data?.Bids) {
return AppResponse.toResponse(response.data.Bids);
constructor(private readonly bidsService: BidsService){}
async getHistoriesBid(lot_id: Bid['lot_id']) {
const bid= await this.bidsService.bidsRepo.findOne({where: {lot_id, }, relations: {web_bid: true}})
try {
switch(bid.web_bid.origin_url){
case 'https://www.grays.com': {
const response = await axios({
url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}&currencyCode=AUD`,
});
if (response.data && response.data?.Bids) {
return AppResponse.toResponse(response.data.Bids);
}
return AppResponse.toResponse([])
}
case 'https://www.pickles.com.au': {
const response = await axios({
url: `https://www.pickles.com.au/PWR-Web/services/api/bidHistoryService/bidHistory?item=${lot_id}`,
});
if (response.data) {
return AppResponse.toResponse(response.data);
}
return AppResponse.toResponse([])
}
default:
return AppResponse.toResponse([])
}
} catch (error) {
return AppResponse.toResponse([]);
}

View File

@ -1,4 +1,4 @@
import { Controller, Post } from '@nestjs/common';
import { Controller, Get, Post } from '@nestjs/common';
import { DashboardService } from '../../services/dashboard.service';
@Controller('admin/dashboards')
@ -14,4 +14,9 @@ export class AdminDashboardController {
async shutdownTool() {
return await this.dashboardService.shutdownTool();
}
@Get('status-tool')
async statusTool() {
return await this.dashboardService.statusTool();
}
}

View File

@ -21,6 +21,7 @@ import { BidsService } from '../../services/bids.service';
import { WebBidsService } from '../../services/web-bids.service';
import { Event } from '../../utils/events';
import AppResponse from '@/response/app-response';
import { ClientUpdateLoginStatusDto } from '../../dto/bid/client-update-login-status.dto';
@Controller('bids')
export class BidsController {
@ -68,17 +69,24 @@ export class BidsController {
return this.bidsService.updateStatusWork(id, type, image);
}
@Post('update-login-status')
async updateLoginStatus(
@Body() data: ClientUpdateLoginStatusDto
) {
return await this.bidsService.emitLoginStatus(data)
}
@Post('test')
async test(@Body('code') code: string) {
const webBid = await this.webBidService.webBidRepo.findOne({
// where: { id: 9 },
where: { id: 8 },
where: { id: 4 },
// where: { id: 1 },
});
this.eventEmitter.emit(Event.verifyCode(webBid), {
code,
// name: 'LAWSONS',
name: 'LANGTONS',
name: 'LAWSONS',
// name: 'LANGTONS',
web_bid: plainToClass(WebBid, webBid),
});

View File

@ -0,0 +1,12 @@
import { IsBoolean, IsNumber, IsObject, IsOptional, IsString } from 'class-validator';
import { WebBid } from '../../entities/wed-bid.entity';
export class ClientUpdateLoginStatusDto {
@IsObject()
data: WebBid
@IsBoolean()
login_status: boolean
}

View File

@ -47,6 +47,17 @@ export class AdminBidGateway implements OnGatewayConnection {
this.server.emit(Event.WORKING, data);
});
this.eventEmitter.onAny(
(
event: string,
payload: { login_status: string; data: WebBid },
) => {
if (!event.startsWith(Event.LOGIN_STATUS)) return;
this.server.emit(Event.statusLogin(payload.data), payload);
},
);
// IMAP
this.imapService.connectIMAP();
}

View File

@ -32,6 +32,7 @@ import { WebBidsService } from './web-bids.service';
import { NotificationService } from '@/modules/notification/notification.service';
import { Event } from '../utils/events';
import _ from 'lodash';
import { ClientUpdateLoginStatusDto } from '../dto/bid/client-update-login-status.dto';
@Injectable()
export class BidsService {
@ -273,7 +274,7 @@ export class BidsService {
const result = await this.bidsRepo.save({
...bid,
...data,
current_price: Math.max(data.current_price, bid.current_price),
current_price: Math.max(data?.current_price || 0, bid.current_price),
updated_at: new Date(), // Cập nhật timestamp
});
@ -508,4 +509,12 @@ export class BidsService {
return AppResponse.toResponse(files);
}
async emitLoginStatus(data: ClientUpdateLoginStatusDto){
this.eventEmitter.emit(Event.statusLogin(data.data), data)
return AppResponse.toResponse(true)
}
}

View File

@ -7,7 +7,7 @@ export class DashboardService {
private readonly tool_name = 'auto-bid-tool';
async resetToolByName(toolName: string): Promise<string> {
async resetProcessByName(toolName: string): Promise<string> {
return new Promise((resolve, reject) => {
// Lấy danh sách process đang chạy
exec('pm2 jlist', (error, stdout, stderr) => {
@ -41,7 +41,35 @@ export class DashboardService {
});
}
async shutdownToolByName(toolName: string): Promise<string> {
async getStatusProcessByName(toolName: string): Promise<string> {
return new Promise((resolve, reject) => {
exec('pm2 jlist', (error, stdout, stderr) => {
if (error) {
return reject(`Error get list process: ${stderr}`);
}
try {
const processList = JSON.parse(stdout);
const targetProcess = processList.find(
(proc: any) => proc.name === toolName,
);
if (!targetProcess) {
return reject(`Not found process for name "${toolName}"`);
}
const status = targetProcess.pm2_env?.status || 'unknown';
return resolve(status); // Trả về: 'online', 'stopped', 'errored', etc.
} catch (parseErr) {
reject(`Error parse JSON output: ${parseErr}`);
}
});
});
}
async shutdownProcessByName(toolName: string): Promise<string> {
return new Promise((resolve, reject) => {
// Lấy danh sách process đang chạy
exec('pm2 jlist', (error, stdout, stderr) => {
@ -77,7 +105,7 @@ export class DashboardService {
async resetTool() {
try {
await this.resetToolByName(this.tool_name);
await this.resetProcessByName(this.tool_name);
return AppResponse.toResponse(true);
} catch (error) {
@ -87,11 +115,22 @@ export class DashboardService {
async shutdownTool() {
try {
await this.shutdownToolByName(this.tool_name);
await this.shutdownProcessByName(this.tool_name);
return AppResponse.toResponse(true);
} catch (error) {
return AppResponse.toResponse(false);
}
}
async statusTool() {
try {
const result = await this.getStatusProcessByName(this.tool_name);
return AppResponse.toResponse(result);
} catch (error) {
return AppResponse.toResponse(false);
}
}
}

View File

@ -6,8 +6,13 @@ export class Event {
public static BIDS_UPDATED = 'bidsUpdated';
public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated';
public static WEB_UPDATED = 'webUpdated';
public static LOGIN_STATUS = 'login-status';
public static verifyCode(data: WebBid) {
return `${this.VERIFY_CODE}.${data.origin_url}`;
}
public static statusLogin(data: WebBid) {
return `${this.LOGIN_STATUS}.${data.origin_url}`;
}
}

View File

@ -7,18 +7,21 @@ import { TypeOrmModule } from '@nestjs/typeorm';
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_NAME'),
charset: 'utf8mb4_unicode_ci',
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize:
configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
}),
useFactory: (configService: ConfigService) => {
return {
type: 'mysql',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_NAME'),
charset: 'utf8mb4_unicode_ci',
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize:
configService.get<string>('ENVIRONMENT') === 'prod' ? false : true,
}
},
}),
],
})

View File

@ -12,6 +12,10 @@ export function extractModelId(url: string): string | null {
const match = url.split('_');
return match ? match[1] : null;
}
case 'https://www.pickles.com.au': {
const model = url.split('/').pop();
return model ? model : null;
}
}
}

View File

@ -0,0 +1,16 @@
module.exports = {
apps: [
{
name: "auto-bid-tool",
script: "./index.js",
instances: 1,
exec_mode: "fork",
watch: false,
log_date_format: "YYYY-MM-DD HH:mm:ss",
output: "./logs/out.log",
error: "./logs/error.log",
merge_logs: true,
max_memory_restart: "12G",
},
],
};

View File

@ -1,11 +1,17 @@
import 'dotenv/config';
import _ from 'lodash';
import pLimit from 'p-limit';
import { io } from 'socket.io-client';
import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } 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 "dotenv/config";
import _ from "lodash";
import pLimit from "p-limit";
import { io } from "socket.io-client";
import {
createApiBid,
createBidProduct,
deleteProfile,
shouldUpdateProductTab,
} 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 { updateLoginStatus } from "./system/apis/bid.js";
global.IS_CLEANING = true;
@ -14,260 +20,335 @@ let MANAGER_BIDS = [];
const activeTasks = new Set();
const handleUpdateProductTabs = (data) => {
if (!Array.isArray(data)) {
console.log('Data must be array');
return;
}
if (!Array.isArray(data)) {
console.log("Data must be array");
return;
}
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid]));
const newDataManager = data.map(({ children, ...web }) => {
const prevApiBid = managerBidMap.get(web.id);
const newDataManager = data.map(({ children, ...web }) => {
const prevApiBid = managerBidMap.get(web.id);
const newChildren = children.map((item) => {
const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id);
const newChildren = children.map((item) => {
const prevProductTab = prevApiBid?.children.find((i) => i.id === item.id);
if (prevProductTab) {
prevProductTab.setNewData(item);
if (prevProductTab) {
prevProductTab.setNewData(item);
return prevProductTab;
}
return prevProductTab;
}
return createBidProduct(web, item);
});
if (prevApiBid) {
prevApiBid.setNewData({ children: newChildren, ...web });
return prevApiBid;
}
return createApiBid({ ...web, children: newChildren });
return createBidProduct(web, item);
});
MANAGER_BIDS = newDataManager;
if (prevApiBid) {
prevApiBid.setNewData({ children: newChildren, ...web });
return prevApiBid;
}
return createApiBid({ ...web, children: newChildren });
});
MANAGER_BIDS = newDataManager;
};
const tracking = async () => {
console.log('🚀 Tracking process started...');
console.log("🚀 Tracking process started...");
while (true) {
try {
console.log('🔍 Scanning active bids...');
const productTabs = _.flatMap(MANAGER_BIDS, 'children');
while (true) {
try {
console.log("🔍 Scanning active bids...");
const productTabs = _.flatMap(MANAGER_BIDS, "children");
await Promise.allSettled(
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
return apiBid.listen_events();
}),
await Promise.allSettled(
MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => {
console.log(`🎧 Listening to events for API Bid ID: ${apiBid.id}`);
return apiBid.listen_events();
})
);
Promise.allSettled(
productTabs.map(async (productTab) => {
console.log(`📌 Processing Product ID: ${productTab.id}`);
// Xác định parent context
if (!productTab.parent_browser_context) {
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
productTab.parent_browser_context = parent?.browser_context;
if (!productTab.parent_browser_context) {
console.log(
`⏳ Waiting for parent process... (Product ID: ${productTab.id})`
);
return;
}
}
// Kết nối Puppeteer nếu chưa có page_context
if (!productTab.page_context) {
console.log(
`🔌 Connecting to page for Product ID: ${productTab.id}`
);
await productTab.puppeteer_connect();
}
Promise.allSettled(
productTabs.map(async (productTab) => {
console.log(`📌 Processing Product ID: ${productTab.id}`);
// Xác định parent context
if (!productTab.parent_browser_context) {
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id });
productTab.parent_browser_context = parent?.browser_context;
if (!productTab.parent_browser_context) {
console.log(`⏳ Waiting for parent process... (Product ID: ${productTab.id})`);
return;
}
}
// Kết nối Puppeteer nếu chưa có page_context
if (!productTab.page_context) {
console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
await productTab.puppeteer_connect();
}
// Kiểm tra URL và điều hướng nếu cần
if ((await productTab.page_context.url()) !== productTab.url) {
console.log(`🔄 Redirecting to new URL for Product ID: ${productTab.id}`);
await productTab.gotoLink();
}
// Cập nhật nếu cần thiết
if (shouldUpdateProductTab(productTab)) {
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
await productTab.update();
} else {
console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`);
}
// Chờ first bid
if (!productTab.first_bid) {
console.log(`🎯 Waiting for first bid for Product ID: ${productTab.id}`);
return;
}
// Kiểm tra thời gian bid
if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) {
console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`);
return;
}
// Thực thi hành động
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
await productTab.action();
}),
// Kiểm tra URL và điều hướng nếu cần
if ((await productTab.page_context.url()) !== productTab.url) {
console.log(
`🔄 Redirecting to new URL for Product ID: ${productTab.id}`
);
await productTab.gotoLink();
}
// Dọn dẹp tab không dùng
console.log('🧹 Cleaning up unused tabs...');
clearLazyTab();
// Cập nhật nếu cần thiết
if (shouldUpdateProductTab(productTab)) {
console.log(`🔄 Updating Product ID: ${productTab.id}...`);
await productTab.update();
} else {
console.log(
`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`
);
}
// Cập nhật trạng thái tracking
console.log('📊 Tracking work status...');
workTracking();
} catch (error) {
console.error('❌ Error in tracking loop:', error);
}
// Chờ first bid
if (!productTab.first_bid) {
console.log(
`🎯 Waiting for first bid for Product ID: ${productTab.id}`
);
return;
}
console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`);
await delay(configs.AUTO_TRACKING_DELAY);
// Kiểm tra thời gian bid
if (
productTab.start_bid_time &&
!isTimeReached(productTab.start_bid_time)
) {
console.log(
`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`
);
return;
}
// Thực thi hành động
console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
await productTab.action();
})
);
// Dọn dẹp tab không dùng
console.log("🧹 Cleaning up unused tabs...");
clearLazyTab();
// Cập nhật trạng thái tracking
console.log("📊 Tracking work status...");
workTracking();
// Bắn event status login
console.log("📊 Tracking login status...");
trackingLoginStatus();
} catch (error) {
console.error("❌ Error in tracking loop:", error);
}
console.log(
`⏳ Waiting ${
configs.AUTO_TRACKING_DELAY / 1000
} seconds before the next iteration...`
);
await delay(configs.AUTO_TRACKING_DELAY);
}
};
const clearLazyTab = async () => {
if (!global.IS_CLEANING) {
console.log('🚀 Cleaning flag is OFF. Proceeding with operation.');
return;
}
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;
}
if (!browser) {
console.warn("⚠️ Browser is not available or disconnected.");
return;
}
try {
const pages = await browser.pages();
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
// 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()),
);
console.log(
"🔍 Page URLs:",
pages.map((page) => page.url())
);
for (const page of pages) {
const pageUrl = page.url();
for (const page of pages) {
const pageUrl = page.url();
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL
if (!pageUrl || pageUrl === 'about:blank') continue;
// 🔥 Bỏ qua tab 'about:blank' hoặc tab không có 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);
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);
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`);
}
}
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`);
}
}
} catch (err) {
console.error('❌ Error in clearLazyTab:', err.message);
}
}
} catch (err) {
console.error("❌ Error in clearLazyTab:", err.message);
}
};
const workTracking = async () => {
try {
const activeData = _.flatMap(MANAGER_BIDS, (item) => [item, ...item.children]);
const limit = pLimit(5);
try {
const activeData = _.flatMap(MANAGER_BIDS, (item) => [
item,
...item.children,
]);
const limit = pLimit(5);
await Promise.allSettled(
activeData
.filter((item) => item.page_context && !item.page_context.isClosed())
.filter((item) => !activeTasks.has(item.id))
.map((item) =>
limit(async () => {
activeTasks.add(item.id);
try {
await item.handleTakeWorkSnapshot();
} catch (error) {
console.error(`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`, error);
} finally {
activeTasks.delete(item.id);
}
}),
),
);
} catch (error) {
console.error(`[❌ ERROR] Work tracking failed: ${error.message}\n`, error.stack);
await Promise.allSettled(
activeData
.filter((item) => item.page_context && !item.page_context.isClosed())
.filter((item) => !activeTasks.has(item.id))
.map((item) =>
limit(async () => {
activeTasks.add(item.id);
try {
await item.handleTakeWorkSnapshot();
} catch (error) {
console.error(
`[❌ ERROR] Snapshot failed for Product ID: ${item.id}`,
error
);
} finally {
activeTasks.delete(item.id);
}
})
)
);
} catch (error) {
console.error(
`[❌ ERROR] Work tracking failed: ${error.message}\n`,
error.stack
);
}
};
const trackingLoginStatus = async () => {
try {
if (!MANAGER_BIDS?.length) return;
const results = await Promise.allSettled(
MANAGER_BIDS.map(async (item) => {
try {
const login_status = await item.isLogin();
await updateLoginStatus({
data: {
id: item.id,
type: item.type,
origin_url: item.origin_url,
},
login_status,
});
} catch (err) {
console.warn(
`[⚠️ WARN] Failed to check login for bid ${
item?.id || "unknown"
}: ${err.message}`
);
}
})
);
// Optional: log summary
const failed = results.filter((r) => r.status === "rejected").length;
if (failed) {
console.warn(`[⚠️ WARN] ${failed} login status checks failed.`);
}
} catch (error) {
console.error(
`[❌ ERROR] Login status tracking failed: ${error.message}\n`,
error.stack
);
}
};
(async () => {
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
transports: ['websocket'],
reconnection: true,
extraHeaders: {
Authorization: process.env.CLIENT_KEY,
},
});
const socket = io(`${configs.SOCKET_URL}/bid-ws`, {
transports: ["websocket"],
reconnection: true,
extraHeaders: {
Authorization: process.env.CLIENT_KEY,
},
});
// set socket on global app
global.socket = socket;
// set socket on global app
global.socket = socket;
// listen connect
socket.on('connect', () => {
console.log('✅ Connected to WebSocket server');
console.log('🔗 Socket ID:', socket.id);
});
// listen connect
socket.on("connect", () => {
console.log("✅ Connected to WebSocket server");
console.log("🔗 Socket ID:", socket.id);
});
// listen connect
socket.on('disconnect', () => {
console.log('❌Client key is valid. Disconnected');
});
// listen connect
socket.on("disconnect", () => {
console.log("❌Client key is valid. Disconnected");
});
// listen event
socket.on('bidsUpdated', async (data) => {
console.log('📢 Bids Data:', data);
// listen event
socket.on("bidsUpdated", async (data) => {
console.log("📢 Bids Data:", data);
handleUpdateProductTabs(data);
});
handleUpdateProductTabs(data);
});
socket.on('webUpdated', async (data) => {
console.log('📢 Account was updated:', data);
socket.on("webUpdated", async (data) => {
console.log("📢 Account was updated:", data);
const isDeleted = deleteProfile(data);
const isDeleted = deleteProfile(data);
if (isDeleted) {
console.log('✅ Profile deleted successfully!');
if (isDeleted) {
console.log("✅ Profile deleted successfully!");
const tab = MANAGER_BIDS.find((item) => item.url === data.url);
const tab = MANAGER_BIDS.find((item) => item.url === data.url);
if (!tab) return;
if (!tab) return;
global.IS_CLEANING = false;
await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
global.IS_CLEANING = false;
await Promise.all(tab.children.map((tab) => safeClosePage(tab)));
await safeClosePage(tab);
await safeClosePage(tab);
global.IS_CLEANING = true;
} else {
console.log('⚠️ No profile found to delete.');
}
});
global.IS_CLEANING = true;
} else {
console.log("⚠️ No profile found to delete.");
}
});
// AUTO TRACKING
tracking();
// AUTO TRACKING
tracking();
})();

View File

@ -1,30 +1,44 @@
import BID_TYPE from '../system/bid-type.js';
import CONSTANTS from '../system/constants.js';
import { takeSnapshot } from '../system/utils.js';
import _ from 'lodash';
import BID_TYPE from "../system/bid-type.js";
import CONSTANTS from "../system/constants.js";
import { takeSnapshot } from "../system/utils.js";
import _ from "lodash";
export class Bid {
type;
puppeteer_connect;
url;
action;
page_context;
type;
puppeteer_connect;
url;
action;
page_context;
constructor(type, url, puppeteer_connect) {
this.type = type;
this.url = url;
this.puppeteer_connect = puppeteer_connect;
constructor(type, url, puppeteer_connect) {
this.type = type;
this.url = url;
this.puppeteer_connect = puppeteer_connect;
}
handleTakeWorkSnapshot = _.debounce(async () => {
if (!this.page_context) return;
try {
// await this.page_context.waitForSelector('#pageContainer', { timeout: 10000 });
console.log(
`✅ Page fully loaded. Taking snapshot for ${
this.type === BID_TYPE.PRODUCT_TAB ? "Product ID" : "Tracking ID"
}: ${this.id}`
);
takeSnapshot(
this.page_context,
this,
"working",
CONSTANTS.TYPE_IMAGE.WORK
);
} catch (error) {
console.error(
`❌ Error taking snapshot for Product ID: ${this.id}:`,
error.message
);
}
}, 1000);
handleTakeWorkSnapshot = _.debounce(async () => {
if (!this.page_context) return;
try {
// await this.page_context.waitForSelector('#pageContainer', { timeout: 10000 });
console.log(`✅ Page fully loaded. Taking snapshot for ${this.type === BID_TYPE.PRODUCT_TAB ? 'Product ID' : 'Tracking ID'}: ${this.id}`);
takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK);
} catch (error) {
console.error(`❌ Error taking snapshot for Product ID: ${this.id}:`, error.message);
}
}, 1000);
async isLogin() {}
}

View File

@ -1,226 +1,292 @@
import path from 'path';
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
import configs from '../../system/config.js';
import { delay, extractNumber, getPathProfile, isTimeReached, safeClosePage } from '../../system/utils.js';
import { ApiBid } from '../api-bid.js';
import fs from 'fs';
import path from "path";
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
import configs from "../../system/config.js";
import {
delay,
extractNumber,
getPathProfile,
isTimeReached,
safeClosePage,
} from "../../system/utils.js";
import { ApiBid } from "../api-bid.js";
import fs from "fs";
export class GrayApiBid extends ApiBid {
retry_login = 0;
retry_login_count = 3;
retry_login = 0;
retry_login_count = 3;
constructor({ ...prev }) {
super(prev);
}
constructor({ ...prev }) {
super(prev);
}
async polling(page) {
try {
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
// page.removeAllListeners('request');
// await page.setRequestInterception(true);
async polling(page) {
try {
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
// page.removeAllListeners('request');
// await page.setRequestInterception(true);
// page.on('request', (request) => {
// if (request.url().includes('api/Notifications/GetOutBidLots')) {
// console.log('🚀 Fake response cho request:', request.url());
// page.on('request', (request) => {
// if (request.url().includes('api/Notifications/GetOutBidLots')) {
// console.log('🚀 Fake response cho request:', request.url());
// const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
// const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8');
// request.respond({
// status: 200,
// contentType: 'application/json',
// body: fakeData,
// });
// } else {
// try {
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
// } catch (error) {
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
// }
// }
// });
// request.respond({
// status: 200,
// contentType: 'application/json',
// body: fakeData,
// });
// } else {
// try {
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
// } catch (error) {
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
// }
// }
// });
console.log(`🔄 [${this.id}] Starting polling process...`);
console.log(`🔄 [${this.id}] Starting polling process...`);
await page.evaluateHandle(
(apiUrl, interval, bidId) => {
if (window._autoBidPollingStarted) {
console.log(`✅ [${bidId}] Polling is already running. Skipping initialization.`);
return;
}
console.log(`🚀 [${bidId}] Initializing polling...`);
window._autoBidPollingStarted = true;
function sendRequest() {
console.log(`📡 [${bidId}] Sending request to track out-bid lots...`);
fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: JSON.stringify({ timeStamp: new Date().getTime() }),
})
.then((response) => console.log(`✅ [${bidId}] Response received: ${response.status}`))
.catch((err) => console.error(`⚠️ [${bidId}] Request error:`, err));
}
window._pollingInterval = setInterval(sendRequest, interval);
},
configs.WEB_CONFIGS.GRAYS.API_CALL_TO_TRACKING,
configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
this.id,
await page.evaluateHandle(
(apiUrl, interval, bidId) => {
if (window._autoBidPollingStarted) {
console.log(
`✅ [${bidId}] Polling is already running. Skipping initialization.`
);
} catch (error) {
if (error.message.includes('Execution context was destroyed')) {
console.log(`⚠️ [${this.id}] Page reload detected, restarting polling...`);
await page.waitForNavigation({ waitUntil: 'networkidle2' }).catch(() => {});
return await this.polling(page);
}
console.error(`🚨 [${this.id}] Unexpected polling error:`, error);
throw error;
}
}
async handleCreateLogsOnServer(data) {
if (!Array.isArray(data)) return;
const values = data.map((item) => {
return {
model: item.Sku,
lot_id: item.Id,
out_price: extractNumber(item.Bid) || 0,
raw_data: JSON.stringify(item),
};
});
await createOutBidLog(values);
}
listen_out_bids = async (data) => {
if (this.children.length <= 0 || data.length <= 0) return;
// SAVE LOGS ON SERVER
this.handleCreateLogsOnServer(data);
const bidOutLots = data.filter((bid) => !this.children_processing.some((item) => item.model === bid.Sku));
const handleChildren = this.children.filter((item) => bidOutLots.some((i) => i.Sku === item.model));
console.log({ handleChildren, children_processing: this.children_processing, data, bidOutLots });
for (const product_tab of handleChildren) {
if (!isTimeReached(product_tab.start_bid_time)) {
console.log(`❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`);
return;
}
this.children_processing.push(product_tab);
if (!product_tab.page_context) {
await product_tab.puppeteer_connect();
}
await product_tab.action();
this.children_processing = this.children_processing.filter((item) => item.id !== product_tab.id);
}
};
async handleLogin() {
const page = this.page_context;
global.IS_CLEANING = false;
const filePath = getPathProfile(this.origin_url);
// 🔍 Check if already logged in (login input should not be visible)
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
console.log(`✅ [${this.id}] Already logged in, skipping login.`);
global.IS_CLEANING = true;
this.retry_login = 0; // Reset retry count
return;
}
}
console.log(`🔑 [${this.id}] Starting login process...`);
console.log(`🚀 [${bidId}] Initializing polling...`);
window._autoBidPollingStarted = true;
try {
await page.type('input[name="username"]', this.username, { delay: 100 });
await page.type('input[name="password"]', this.password, { delay: 150 });
await page.click('#loginButton');
function sendRequest() {
console.log(
`📡 [${bidId}] Sending request to track out-bid lots...`
);
fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: JSON.stringify({ timeStamp: new Date().getTime() }),
})
.then((response) =>
console.log(
`✅ [${bidId}] Response received: ${response.status}`
)
)
.catch((err) =>
console.error(`⚠️ [${bidId}] Request error:`, err)
);
}
await Promise.race([
page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }),
page.waitForFunction(() => !document.querySelector('input[name="username"]'), { timeout: 8000 }), // Check if login input disappears
]);
window._pollingInterval = setInterval(sendRequest, interval);
},
configs.WEB_CONFIGS.GRAYS.API_CALL_TO_TRACKING,
configs.WEB_CONFIGS.GRAYS.AUTO_CALL_API_TO_TRACKING,
this.id
);
} catch (error) {
if (error.message.includes("Execution context was destroyed")) {
console.log(
`⚠️ [${this.id}] Page reload detected, restarting polling...`
);
await page
.waitForNavigation({ waitUntil: "networkidle2" })
.catch(() => {});
return await this.polling(page);
}
if (!(await page.$('input[name="username"]'))) {
console.log(`✅ [${this.id}] Login successful!`);
this.retry_login = 0; // Reset retry count after success
return;
}
console.error(`🚨 [${this.id}] Unexpected polling error:`, error);
throw error;
}
}
throw new Error('Login failed, login input is still visible.');
} catch (error) {
console.log(`⚠️ [${this.id}] Login error: ${error.message}. Retrying attempt ${this.retry_login + 1}`);
async handleCreateLogsOnServer(data) {
if (!Array.isArray(data)) return;
this.retry_login++;
if (this.retry_login > this.retry_login_count) {
console.log(`🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`);
safeClosePage(this);
this.retry_login = 0; // Reset retry count
return;
}
const values = data.map((item) => {
return {
model: item.Sku,
lot_id: item.Id,
out_price: extractNumber(item.Bid) || 0,
raw_data: JSON.stringify(item),
};
});
safeClosePage(this); // Close the current page
await delay(1000);
await createOutBidLog(values);
}
if (!this.page_context) {
await this.puppeteer_connect(); // Reconnect if page is closed
}
listen_out_bids = async (data) => {
if (this.children.length <= 0 || data.length <= 0) return;
return await this.action(); // Retry login
} finally {
global.IS_CLEANING = true;
}
// SAVE LOGS ON SERVER
this.handleCreateLogsOnServer(data);
const bidOutLots = data.filter(
(bid) => !this.children_processing.some((item) => item.model === bid.Sku)
);
const handleChildren = this.children.filter((item) =>
bidOutLots.some((i) => i.Sku === item.model)
);
console.log({
handleChildren,
children_processing: this.children_processing,
data,
bidOutLots,
});
for (const product_tab of handleChildren) {
if (!isTimeReached(product_tab.start_bid_time)) {
console.log(
`❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`
);
return;
}
this.children_processing.push(product_tab);
if (!product_tab.page_context) {
await product_tab.puppeteer_connect();
}
await product_tab.action();
this.children_processing = this.children_processing.filter(
(item) => item.id !== product_tab.id
);
}
};
isLogin = async () => {
if (!this.page_context) return false;
const filePath = getPathProfile(this.origin_url);
if (
!(await this.page_context.$('input[name="username"]')) ||
fs.existsSync(filePath)
) {
return true;
}
action = async () => {
try {
const page = this.page_context;
return false;
};
await page.goto(this.url, { waitUntil: 'networkidle2' });
console.log(`🌍 [${this.id}] Navigated to URL: ${this.url}`);
async handleLogin() {
const page = this.page_context;
await page.bringToFront();
console.log(`🎯 [${this.id}] Brought page to front.`);
global.IS_CLEANING = false;
// 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');
console.log(`🛠️ [${this.id}] UserAgent set.`);
const filePath = getPathProfile(this.origin_url);
page.on('response', async (response) => {
if (response.request().url().includes('api/Notifications/GetOutBidLots')) {
console.log(`🚀 [${this.id}] API POST detected: ${response.url()}`);
// 🔍 Check if already logged in (login input should not be visible)
if (!(await page.$('input[name="username"]')) || fs.existsSync(filePath)) {
console.log(`✅ [${this.id}] Already logged in, skipping login.`);
try {
const responseBody = await response.json();
await this.listen_out_bids(responseBody.AuctionOutBidLots || []);
} catch (error) {
console.error(`❌ [${this.id}] Error processing response:`, error?.message);
}
}
});
global.IS_CLEANING = true;
this.retry_login = 0; // Reset retry count
return;
}
page.on('load', async () => {
console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`);
await this.polling(page);
await this.handleLogin();
});
console.log(`🔑 [${this.id}] Starting login process...`);
await this.polling(page); // Call when first load
await this.handleLogin();
} catch (error) {
console.log(`❌ [${this.id}] Action error: ${error.message}`);
try {
await page.type('input[name="username"]', this.username, { delay: 100 });
await page.type('input[name="password"]', this.password, { delay: 150 });
await page.click("#loginButton");
await Promise.race([
page.waitForNavigation({
timeout: 8000,
waitUntil: "domcontentloaded",
}),
page.waitForFunction(
() => !document.querySelector('input[name="username"]'),
{ timeout: 8000 }
), // Check if login input disappears
]);
if (!(await page.$('input[name="username"]'))) {
console.log(`✅ [${this.id}] Login successful!`);
this.retry_login = 0; // Reset retry count after success
return;
}
throw new Error("Login failed, login input is still visible.");
} catch (error) {
console.log(
`⚠️ [${this.id}] Login error: ${error.message}. Retrying attempt ${
this.retry_login + 1
} `
);
this.retry_login++;
if (this.retry_login > this.retry_login_count) {
console.log(
`🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`
);
safeClosePage(this);
this.retry_login = 0; // Reset retry count
return;
}
safeClosePage(this); // Close the current page
await delay(1000);
if (!this.page_context) {
await this.puppeteer_connect(); // Reconnect if page is closed
}
return await this.action(); // Retry login
} finally {
global.IS_CLEANING = true;
}
}
action = async () => {
try {
const page = this.page_context;
await page.goto(this.url, { waitUntil: "networkidle2" });
console.log(`🌍 [${this.id}] Navigated to URL: ${this.url}`);
await page.bringToFront();
console.log(`🎯 [${this.id}] Brought page to front.`);
// 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"
);
console.log(`🛠️ [${this.id}] UserAgent set.`);
page.on("response", async (response) => {
if (
response.request().url().includes("api/Notifications/GetOutBidLots")
) {
console.log(`🚀 [${this.id}] API POST detected: ${response.url()}`);
try {
const responseBody = await response.json();
await this.listen_out_bids(responseBody.AuctionOutBidLots || []);
} catch (error) {
console.error(
`❌ [${this.id}] Error processing response:`,
error?.message
);
}
}
};
});
page.on("load", async () => {
console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`);
await this.polling(page);
await this.handleLogin();
});
await this.polling(page); // Call when first load
await this.handleLogin();
} catch (error) {
console.log(`❌ [${this.id}] Action error: ${error.message}`);
}
};
}

View File

@ -1,245 +1,322 @@
import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js';
import CONSTANTS from '../../system/constants.js';
import { delay, extractNumber, isNumber, isTimeReached, removeFalsyValues, safeClosePage, takeSnapshot } from '../../system/utils.js';
import { ProductBid } from '../product-bid.js';
import {
outBid,
pushPrice,
updateBid,
updateStatusByPrice,
} from "../../system/apis/bid.js";
import CONSTANTS from "../../system/constants.js";
import {
delay,
extractNumber,
isNumber,
isTimeReached,
removeFalsyValues,
safeClosePage,
takeSnapshot,
} from "../../system/utils.js";
import { ProductBid } from "../product-bid.js";
export class GraysProductBid extends ProductBid {
constructor({ ...prev }) {
super(prev);
constructor({ ...prev }) {
super(prev);
}
async validate({ page, price_value }) {
if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
console.log(`❌ [${this.id}] It's not time yet`);
return { result: false, bid_price: 0 };
}
async validate({ page, price_value }) {
if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) {
console.log(`❌ [${this.id}] It's not time yet`);
return { result: false, bid_price: 0 };
}
if (!isNumber(price_value)) {
console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
await takeSnapshot(page, this, "price-value-null");
if (!isNumber(price_value)) {
console.log(`❌ [${this.id}] Can't get PRICE_VALUE`);
await takeSnapshot(page, this, 'price-value-null');
return { result: false, bid_price: 0 };
}
const bid_price = this.plus_price + Number(price_value);
if (bid_price > this.max_price) {
console.log(`${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`);
await takeSnapshot(page, this, 'price-bid-more-than');
await outBid(this.id);
return { result: false, bid_price: 0 };
}
const response = await pushPrice({
bid_id: this.id,
price: bid_price,
});
if (!response.status) {
return { result: false, bid_price: 0 };
}
this.histories = response.data;
// RESET first bid
if (this.histories.length > 0 && this.first_bid) {
this.first_bid = false;
}
return { result: true, bid_price };
return { result: false, bid_price: 0 };
}
getCloseTime = async () => {
try {
if (!this.page_context) return null;
const bid_price = this.plus_price + Number(price_value);
await this.page_context.waitForSelector('#lot-closing-datetime', { timeout: 3000 });
if (bid_price > this.max_price) {
console.log(
`${this.id} PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT`
);
await takeSnapshot(page, this, "price-bid-more-than");
return await this.page_context.$eval('#lot-closing-datetime', (el) => el.value);
} catch (error) {
return null;
}
};
await outBid(this.id);
getPriceWasBid = async () => {
try {
if (!this.page_context) return null;
await this.page_context.waitForSelector('#biddableLot form div div:nth-child(1) span span', { timeout: 3000 });
const element = await this.page_context.$('#biddableLot form div div:nth-child(1) span span');
const textPrice = await this.page_context.evaluate((el) => el.textContent, element);
return extractNumber(textPrice) || null;
} catch (error) {
return null;
}
};
async isCloseProduct() {
const close_time = await this.getCloseTime();
if (!close_time) {
const priceWasBid = await this.getPriceWasBid();
await updateStatusByPrice(this.id, priceWasBid);
return { result: true, close_time: null };
}
await delay(500);
if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
console.log(`❌ [${this.id}] Product is close ${close_time}`);
return { result: true, close_time };
}
return { result: false, close_time };
return { result: false, bid_price: 0 };
}
async handleWritePrice(page, bid_price) {
await page.type('#price', String(bid_price));
await delay(500);
const response = await pushPrice({
bid_id: this.id,
price: bid_price,
});
if (!response.status) {
return { result: false, bid_price: 0 };
}
async placeBid(page) {
try {
await page.click('#bid-type-standard');
await delay(500);
this.histories = response.data;
await page.click('#btnSubmit');
await delay(1000);
await page.waitForSelector('button', { timeout: 5000 });
await delay(500);
await page.click('button');
await page.waitForNavigation({ timeout: 5000 });
await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
return true;
} catch (error) {
console.log(`❌ [${this.id}] Timeout to loading`);
await takeSnapshot(page, this, 'timeout to loading');
return false;
}
// RESET first bid
if (this.histories.length > 0 && this.first_bid) {
this.first_bid = false;
}
async handleReturnProductPage(page) {
await page.goto(this.url);
await delay(1000);
return { result: true, bid_price };
}
getCloseTime = async () => {
try {
if (!this.page_context) return null;
await this.page_context.waitForSelector("#lot-closing-datetime", {
timeout: 3000,
});
return await this.page_context.$eval(
"#lot-closing-datetime",
(el) => el.value
);
} catch (error) {
return null;
}
};
getPriceWasBid = async () => {
try {
if (!this.page_context) return null;
await this.page_context.waitForSelector(
"#biddableLot form div div:nth-child(1) span span",
{ timeout: 3000 }
);
const element = await this.page_context.$(
"#biddableLot form div div:nth-child(1) span span"
);
const textPrice = await this.page_context.evaluate(
(el) => el.textContent,
element
);
return extractNumber(textPrice) || null;
} catch (error) {
return null;
}
};
async isCloseProduct() {
const close_time = await this.getCloseTime();
if (!close_time) {
const priceWasBid = await this.getPriceWasBid();
await updateStatusByPrice(this.id, priceWasBid);
return { result: true, close_time: null };
}
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
await delay(500);
if (response) {
this.lot_id = response.lot_id;
this.close_time = response.close_time;
this.start_bid_time = response.start_bid_time;
}
if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
console.log(`❌ [${this.id}] Product is close ${close_time}`);
return { result: true, close_time };
}
update = async () => {
if (!this.page_context) return;
return { result: false, close_time };
}
const page = this.page_context;
async handleWritePrice(page, bid_price) {
await page.type("#price", String(bid_price));
await delay(500);
}
try {
const close_time = await this.getCloseTime();
async placeBid(page) {
try {
await page.click("#bid-type-standard");
await delay(500);
// Chờ phần tử xuất hiện trước khi lấy giá trị
await page.waitForSelector('#priceValue', { timeout: 5000 }).catch(() => null);
const price_value = await page.$eval('#priceValue', (el) => el.value).catch(() => null);
await page.click("#btnSubmit");
await delay(1000);
await page.waitForSelector('#lotId', { timeout: 5000 }).catch(() => null);
const lot_id = await page.$eval('#lotId', (el) => el.value).catch(() => null);
await page.waitForSelector("button", { timeout: 5000 });
await page.waitForSelector('#placebid-sticky > div:nth-child(2) > div > h3', { timeout: 5000 }).catch(() => null);
const name = await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText).catch(() => null);
await delay(500);
await page
.waitForSelector('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', { timeout: 5000 })
.catch(() => null);
const current_price = await page
.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)
.catch(() => null);
await page.click("button");
console.log(`📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`);
await page.waitForNavigation({ timeout: 5000 });
const data = removeFalsyValues(
{
lot_id,
reserve_price: price_value,
close_time: close_time ? String(close_time) : null,
name,
current_price: current_price ? extractNumber(current_price) : null,
},
['close_time'],
);
await takeSnapshot(
page,
this,
"bid-success",
CONSTANTS.TYPE_IMAGE.SUCCESS
);
return true;
} catch (error) {
console.log(`❌ [${this.id}] Timeout to loading`);
await takeSnapshot(page, this, "timeout to loading");
return false;
}
}
this.handleUpdateBid(data);
async handleReturnProductPage(page) {
await page.goto(this.url);
await delay(1000);
}
return { price_value, lot_id, name, current_price };
} catch (error) {
console.error(`🚨 Error updating product info: ${error.message}`);
return null;
}
};
async handleUpdateBid({
lot_id,
close_time,
name,
current_price,
reserve_price,
}) {
const response = await updateBid(this.id, {
lot_id,
close_time,
name,
current_price,
reserve_price: Number(reserve_price) || 0,
});
action = async () => {
try {
const page = this.page_context;
if (response) {
this.lot_id = response.lot_id;
this.close_time = response.close_time;
this.start_bid_time = response.start_bid_time;
}
}
await this.gotoLink();
console.log(`🌍 [${this.id}] Navigated to link.`);
update = async () => {
if (!this.page_context) return;
await delay(1000);
const page = this.page_context;
const { close_time, ...isCloseProduct } = await this.isCloseProduct();
if (isCloseProduct.result) {
console.log(`❌ [${this.id}] The product is closed, cannot place a bid.`);
return;
}
try {
const close_time = await this.getCloseTime();
await delay(500);
// Chờ phần tử xuất hiện trước khi lấy giá trị
await page
.waitForSelector("#priceValue", { timeout: 5000 })
.catch(() => null);
const price_value = await page
.$eval("#priceValue", (el) => el.value)
.catch(() => null);
const { price_value } = await this.update();
if (!price_value) return;
await page.waitForSelector("#lotId", { timeout: 5000 }).catch(() => null);
const lot_id = await page
.$eval("#lotId", (el) => el.value)
.catch(() => null);
const { result, bid_price } = await this.validate({ page, price_value });
if (!result) {
console.log(`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`);
return;
}
await page
.waitForSelector("#placebid-sticky > div:nth-child(2) > div > h3", {
timeout: 5000,
})
.catch(() => null);
const name = await page
.$eval(".dls-heading-3.lotPageTitle", (el) => el.innerText)
.catch(() => null);
const bidHistoriesItem = _.maxBy(this.histories, 'price');
if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
return;
}
await page
.waitForSelector(
"#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
{ timeout: 5000 }
)
.catch(() => null);
const current_price = await page
.$eval(
"#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span",
(el) => el.innerText
)
.catch(() => null);
if (price_value != bid_price) {
console.log(`✍️ [${this.id}] Updating bid price from ${price_value}${bid_price}`);
await this.handleWritePrice(page, bid_price);
}
console.log(
`📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`
);
console.log(`🚀 [${this.id}] Placing the bid...`);
const resultPlaceBid = await this.placeBid(page);
if (!resultPlaceBid) {
console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
await takeSnapshot(page, this, 'place-bid-action');
return;
}
const data = removeFalsyValues(
{
lot_id,
reserve_price: price_value,
close_time: close_time ? String(close_time) : null,
name,
current_price: current_price ? extractNumber(current_price) : null,
},
["close_time"]
);
console.log(`✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`);
await this.handleReturnProductPage(page);
} catch (error) {
console.error(`🚨 [${this.id}] Error navigating the page: ${error.message}`);
}
};
this.handleUpdateBid(data);
return { price_value, lot_id, name, current_price };
} catch (error) {
console.error(`🚨 Error updating product info: ${error.message}`);
return null;
}
};
action = async () => {
try {
const page = this.page_context;
await this.gotoLink();
console.log(`🌍 [${this.id}] Navigated to link.`);
await delay(1000);
const { close_time, ...isCloseProduct } = await this.isCloseProduct();
if (isCloseProduct.result) {
console.log(
`❌ [${this.id}] The product is closed, cannot place a bid.`
);
return;
}
await delay(500);
const { price_value } = await this.update();
if (!price_value) return;
const { result, bid_price } = await this.validate({ page, price_value });
if (!result) {
console.log(
`❌ [${this.id}] Validation failed. Unable to proceed with bidding.`
);
return;
}
const bidHistoriesItem = _.maxBy(this.histories, "price");
if (bidHistoriesItem && bidHistoriesItem.price === this.current_price) {
console.log(
`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
);
return;
}
if (price_value != bid_price) {
console.log(
`✍️ [${this.id}] Updating bid price from ${price_value}${bid_price}`
);
await this.handleWritePrice(page, bid_price);
}
console.log(`🚀 [${this.id}] Placing the bid...`);
const resultPlaceBid = await this.placeBid(page);
if (!resultPlaceBid) {
console.log(`❌ [${this.id}] Error occurred while placing the bid.`);
await takeSnapshot(page, this, "place-bid-action");
return;
}
console.log(
`✅ [${this.id}] Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${close_time}`
);
await this.handleReturnProductPage(page);
} catch (error) {
console.error(
`🚨 [${this.id}] Error navigating the page: ${error.message}`
);
}
};
}

View File

@ -1,227 +1,284 @@
import fs from 'fs';
import configs from '../../system/config.js';
import { getPathProfile, safeClosePage } from '../../system/utils.js';
import { ApiBid } from '../api-bid.js';
import _ from 'lodash';
import { updateStatusByPrice } from '../../system/apis/bid.js';
import fs from "fs";
import configs from "../../system/config.js";
import { getPathProfile, safeClosePage } from "../../system/utils.js";
import { ApiBid } from "../api-bid.js";
import _ from "lodash";
import { updateStatusByPrice } from "../../system/apis/bid.js";
export class LangtonsApiBid 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 1 minute.`
)
);
}, 120 * 1000); // 120 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
});
});
isLogin = async () => {
if (!this.page_context) return false;
const filePath = getPathProfile(this.origin_url);
return (
!(await this.page_context.$('input[name="loginEmail"]')) &&
fs.existsSync(filePath)
);
};
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.$('input[name="loginEmail"]')) &&
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 1 minute.`));
}, 120 * 1000); // 120 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.$('input[name="loginEmail"]')) && fs.existsSync(filePath);
if (fs.existsSync(filePath)) {
console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`);
fs.unlinkSync(filePath);
}
async handleLogin() {
const page = this.page_context;
const children = this.children.filter((item) => item.page_context);
console.log(
`🔍 [${this.id}] Found ${children.length} child pages to close.`
);
global.IS_CLEANING = false;
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);
})
);
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.$('input[name="loginEmail"]')) && 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('input[name="loginEmail"]', this.username, { delay: 100 });
// ⌨ Enter password
console.log(`✍ [${this.id}] Entering password...`);
await page.type('input[name="loginPassword"]', this.password, { delay: 150 });
// ✅ Click the "Remember Me" checkbox
console.log(`🔘 [${this.id}] Clicking the "Remember Me" checkbox`);
await page.click('#rememberMe', { delay: 80 });
// 🚀 Click the login button
console.log(`🔘 [${this.id}] Clicking the "Login" button`);
await page.click('#loginFormSubmitButton', { delay: 92 });
// ⏳ Wait for navigation after login
console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
console.log(`🌍 [${this.id}] Current page after login:`, page.url());
// 📢 Listen for verification code event
console.log(`👂 [${this.id}] Listening for event: verify-code.${this.origin_url}`);
// ⏳ Wait for verification code from socket event
const { name, code } = await this.waitVerifyData();
console.log(`✅ [${this.id}] Verification code received:`, { name, code });
// ⌨ 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 });
// ⏳ 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' });
// 📂 Save session context to avoid re-login
await this.saveContext();
console.log(`✅ [${this.id}] Login successful!`);
// await page.goto(this.url);
console.log(`✅ [${this.id}] Navigation successful!`);
} catch (error) {
console.error(`❌ [${this.id}] Error during login process:`, error.message);
} finally {
global.IS_CLEANING = true;
}
console.log(
`➡ [${this.id}] Closing main page context: ${this.page_context}`
);
await safeClosePage(this);
}
async getWonList() {
try {
await page.waitForSelector('.row.account-product-list', { timeout: 30000 });
console.log(`🔑 [${this.id}] Starting login process...`);
const items = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.row.account-product-list')).map((item) => item.getAttribute('data-lotid') || null);
});
try {
// ⌨ Enter email
console.log(`✍ [${this.id}] Entering email:`, this.username);
await page.type('input[name="loginEmail"]', this.username, {
delay: 100,
});
return _.compact(items);
} catch (error) {
return [];
}
// ⌨ Enter password
console.log(`✍ [${this.id}] Entering password...`);
await page.type('input[name="loginPassword"]', this.password, {
delay: 150,
});
// ✅ Click the "Remember Me" checkbox
console.log(`🔘 [${this.id}] Clicking the "Remember Me" checkbox`);
await page.click("#rememberMe", { delay: 80 });
// 🚀 Click the login button
console.log(`🔘 [${this.id}] Clicking the "Login" button`);
await page.click("#loginFormSubmitButton", { delay: 92 });
// ⏳ Wait for navigation after login
console.log(`⏳ [${this.id}] Waiting for navigation after login...`);
await page.waitForNavigation({
timeout: 8000,
waitUntil: "domcontentloaded",
});
console.log(`🌍 [${this.id}] Current page after login:`, page.url());
// 📢 Listen for verification code event
console.log(
`👂 [${this.id}] Listening for event: verify-code.${this.origin_url}`
);
// ⏳ Wait for verification code from socket event
const { name, code } = await this.waitVerifyData();
console.log(`✅ [${this.id}] Verification code received:`, {
name,
code,
});
// ⌨ 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 });
// ⏳ 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" });
// 📂 Save session context to avoid re-login
await this.saveContext();
console.log(`✅ [${this.id}] Login successful!`);
// await page.goto(this.url);
console.log(`✅ [${this.id}] Navigation successful!`);
} catch (error) {
console.error(
`❌ [${this.id}] Error during login process:`,
error.message
);
} finally {
global.IS_CLEANING = true;
}
}
async getWonList() {
try {
await page.waitForSelector(".row.account-product-list", {
timeout: 30000,
});
const items = await page.evaluate(() => {
return Array.from(
document.querySelectorAll(".row.account-product-list")
).map((item) => item.getAttribute("data-lotid") || null);
});
return _.compact(items);
} catch (error) {
return [];
}
}
async handleUpdateWonItem() {
console.log(`🔄 [${this.id}] Starting to update the won list...`);
// Lấy danh sách các lot_id thắng
const items = await this.getWonList();
console.log(`📌 [${this.id}] List of won lot_ids:`, items);
// Nếu không có item nào, thoát ra
if (items.length === 0) {
console.log(`⚠️ [${this.id}] No items to update.`);
return;
}
async handleUpdateWonItem() {
console.log(`🔄 [${this.id}] Starting to update the won list...`);
// Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
const result = _.filter(this.children, (item) =>
_.includes(items, item.lot_id)
);
console.log(
`✅ [${this.id}] ${result.length} items need to be updated:`,
result
);
// Lấy danh sách các lot_id thắng
const items = await this.getWonList();
console.log(`📌 [${this.id}] List of won lot_ids:`, items);
// Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
const responses = await Promise.allSettled(
result.map((i) => updateStatusByPrice(i.id, i.current_price))
);
// Nếu không có item nào, thoát ra
if (items.length === 0) {
console.log(`⚠️ [${this.id}] No items to update.`);
return;
// Log kết quả của mỗi request
responses.forEach((response, index) => {
if (response.status === "fulfilled") {
console.log(`✔️ [${this.id}] Successfully updated:`, result[index]);
} else {
console.error(
`❌ [${this.id}] Update failed:`,
result[index],
response.reason
);
}
});
console.log(`🏁 [${this.id}] Finished updating the won list.`);
return responses;
}
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.LANGTONS.LOGIN_URL)) {
await this.handleLogin();
}
}
});
// Lọc danh sách `this.children` chỉ giữ lại những item có trong danh sách thắng
const result = _.filter(this.children, (item) => _.includes(items, item.lot_id));
console.log(`✅ [${this.id}] ${result.length} items need to be updated:`, result);
await page.goto(this.url, { waitUntil: "networkidle2" });
// Gọi API updateStatusByPrice cho mỗi item và đợi tất cả hoàn thành
const responses = await Promise.allSettled(result.map((i) => updateStatusByPrice(i.id, i.current_price)));
await page.bringToFront();
// Log kết quả của mỗi request
responses.forEach((response, index) => {
if (response.status === 'fulfilled') {
console.log(`✔️ [${this.id}] Successfully updated:`, result[index]);
} else {
console.error(`❌ [${this.id}] Update failed:`, result[index], response.reason);
}
});
console.log(`🏁 [${this.id}] Finished updating the won list.`);
return responses;
// 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);
}
};
action = async () => {
try {
const page = this.page_context;
listen_events = async () => {
if (this.page_context) return;
page.on('response', async (response) => {
const request = response.request();
if (request.redirectChain().length > 0) {
if (response.url().includes(configs.WEB_CONFIGS.LANGTONS.LOGIN_URL)) {
await this.handleLogin();
}
}
});
await this.puppeteer_connect();
await this.action();
await page.goto(this.url, { waitUntil: 'networkidle2' });
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.`);
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);
// this.handleUpdateWonItem();
} else {
console.log(
`❌ [${this.id}] Page context is closed. Stopping reload.`
);
clearInterval(this.reloadInterval);
}
};
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.`);
// this.handleUpdateWonItem();
} 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

@ -1,409 +1,510 @@
import _ from 'lodash';
import { outBid, pushPrice, updateBid } from '../../system/apis/bid.js';
import { sendMessage } from '../../system/apis/notification.js';
import { createOutBidLog } from '../../system/apis/out-bid-log.js';
import configs from '../../system/config.js';
import CONSTANTS from '../../system/constants.js';
import { convertAETtoUTC, isTimeReached, removeFalsyValues, takeSnapshot } from '../../system/utils.js';
import { ProductBid } from '../product-bid.js';
import _ from "lodash";
import { outBid, pushPrice, updateBid } from "../../system/apis/bid.js";
import { sendMessage } from "../../system/apis/notification.js";
import { createOutBidLog } from "../../system/apis/out-bid-log.js";
import configs from "../../system/config.js";
import CONSTANTS from "../../system/constants.js";
import {
convertAETtoUTC,
isTimeReached,
removeFalsyValues,
takeSnapshot,
} from "../../system/utils.js";
import { ProductBid } from "../product-bid.js";
export class LangtonsProductBid extends ProductBid {
constructor({ ...prev }) {
super(prev);
constructor({ ...prev }) {
super(prev);
}
// Hàm lấy thời gian kết thúc từ trang web
async getCloseTime() {
try {
// Kiểm tra xem có context của trang web không, nếu không thì trả về null
if (!this.page_context) return null;
await this.page_context.waitForSelector(".site-timezone", {
timeout: 2000,
});
const time = await this.page_context.evaluate(() => {
const el = document.querySelector(".site-timezone");
return el ? el.innerText : null;
});
return time ? convertAETtoUTC(time) : null;
// return new Date(Date.now() + 6 * 60 * 1000).toUTCString();
} catch (error) {
// Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null
return null;
}
}
async waitForApiResponse(timeout = 15000) {
if (!this.page_context) {
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
return null;
}
// Hàm lấy thời gian kết thúc từ trang web
async getCloseTime() {
return new Promise((resolve) => {
const onResponse = async (response) => {
try {
// Kiểm tra xem có context của trang web không, nếu không thì trả về null
if (!this.page_context) return null;
await this.page_context.waitForSelector('.site-timezone', { timeout: 2000 });
const time = await this.page_context.evaluate(() => {
const el = document.querySelector('.site-timezone');
return el ? el.innerText : null;
});
return time ? convertAETtoUTC(time) : null;
// return new Date(Date.now() + 6 * 60 * 1000).toUTCString();
} catch (error) {
// Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null
return null;
}
}
async waitForApiResponse(timeout = 15000) {
if (!this.page_context) {
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
return null;
}
return new Promise((resolve) => {
const onResponse = async (response) => {
try {
if (!response || !response.request().url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
return;
}
clearTimeout(timer); // Hủy timeout nếu có phản hồi
this.page_context.off('response', onResponse); // Gỡ bỏ listener
const data = await response.json();
resolve(data);
} catch (error) {
console.error(`❌ [${this.id}] Error while parsing response:`, error?.message);
resolve(null);
}
};
const timer = setTimeout(async () => {
console.log(`⏳ [${this.id}] Timeout: No response received within ${timeout / 1000}s`);
this.page_context.off('response', onResponse); // Gỡ bỏ listener khi timeout
await this.page_context.reload({ waitUntil: 'networkidle0' }); // reload page
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
resolve(null);
}, timeout);
this.page_context.on('response', onResponse);
});
}
async getName() {
try {
if (!this.page_context) return null;
await this.page_context.waitForSelector('.product-name', { timeout: 3000 });
return await this.page_context.evaluate(() => {
const el = document.querySelector('.product-name');
return el ? el.innerText : null;
});
} catch (error) {
return null;
}
}
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price, model }) {
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0, model });
if (response) {
this.lot_id = response.lot_id;
this.close_time = response.close_time;
this.start_bid_time = response.start_bid_time;
}
}
update = async () => {
if (!this.page_context) return;
console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`);
// 📌 Lấy thời gian kết thúc đấu giá từ giao diện
const close_time = await this.getCloseTime();
console.log(`⏳ [${this.id}] Retrieved close time: ${close_time}`);
// 📌 Lấy tên sản phẩm hoặc thông tin liên quan
const name = await this.getName();
console.log(`📌 [${this.id}] Retrieved name: ${name}`);
// 📌 Chờ phản hồi API từ trang, tối đa 10 giây
const result = await this.waitForApiResponse();
// 📌 Nếu không có dữ liệu trả về thì dừng
if (!result) {
console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
if (
!response ||
!response
.request()
.url()
.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)
) {
return;
}
}
// 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
const data = removeFalsyValues(
{
model: result?.pid || null,
lot_id: result?.lotId || null,
reserve_price: 21, //test
// reserve_price: result.lotData?.minimumBid || null,
// current_price: result.lotData?.currentMaxBid || null,
current_price: 20, // test
// close_time: close_time && !this.close_time ? String(close_time) : null,
close_time: close_time ? String(close_time) : null,
name,
},
// [],
['close_time'],
clearTimeout(timer); // Hủy timeout nếu có phản hồi
this.page_context.off("response", onResponse); // Gỡ bỏ listener
const data = await response.json();
resolve(data);
} catch (error) {
console.error(
`❌ [${this.id}] Error while parsing response:`,
error?.message
);
resolve(null);
}
};
const timer = setTimeout(async () => {
console.log(
`⏳ [${this.id}] Timeout: No response received within ${
timeout / 1000
}s`
);
this.page_context.off("response", onResponse); // Gỡ bỏ listener khi timeout
await this.page_context.reload({ waitUntil: "networkidle0" }); // reload page
console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`);
resolve(null);
}, timeout);
this.page_context.on("response", onResponse);
});
}
async getName() {
try {
if (!this.page_context) return null;
await this.page_context.waitForSelector(".product-name", {
timeout: 3000,
});
return await this.page_context.evaluate(() => {
const el = document.querySelector(".product-name");
return el ? el.innerText : null;
});
} catch (error) {
return null;
}
}
async handleUpdateBid({
lot_id,
close_time,
name,
current_price,
reserve_price,
model,
}) {
const response = await updateBid(this.id, {
lot_id,
close_time,
name,
current_price,
reserve_price: Number(reserve_price) || 0,
model,
});
if (response) {
this.lot_id = response.lot_id;
this.close_time = response.close_time;
this.start_bid_time = response.start_bid_time;
}
}
update = async () => {
if (!this.page_context) return;
console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`);
// 📌 Lấy thời gian kết thúc đấu giá từ giao diện
const close_time = await this.getCloseTime();
console.log(`⏳ [${this.id}] Retrieved close time: ${close_time}`);
// 📌 Lấy tên sản phẩm hoặc thông tin liên quan
const name = await this.getName();
console.log(`📌 [${this.id}] Retrieved name: ${name}`);
// 📌 Chờ phản hồi API từ trang, tối đa 10 giây
const result = await this.waitForApiResponse();
// 📌 Nếu không có dữ liệu trả về thì dừng
if (!result) {
console.log(`⚠️ [${this.id}] No valid data received, skipping update.`);
return;
}
// 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
const data = removeFalsyValues(
{
model: result?.pid || null,
lot_id: result?.lotId || null,
reserve_price: result.lotData?.minimumBid || null,
current_price: result.lotData?.currentMaxBid || null,
// close_time: close_time && !this.close_time ? String(close_time) : null,
close_time: close_time ? String(close_time) : null,
name,
},
// [],
["close_time"]
);
console.log(`🚀 [${this.id}] Processed data ready for update`);
// 📌 Gửi dữ liệu cập nhật lên hệ thống
await this.handleUpdateBid(data);
console.log("✅ Update successful!");
return { ...response, name, close_time };
};
async getContinueShopButton() {
try {
if (!this.page_context) return null;
await this.page_context.waitForSelector(
".btn.btn-block.btn-primary.error.continue-shopping",
{ timeout: 3000 }
);
return await this.page_context.evaluate(() => {
const el = document.querySelector(
".btn.btn-block.btn-primary.error.continue-shopping"
);
console.log(`🚀 [${this.id}] Processed data ready for update`);
return el;
});
} catch (error) {
return null;
}
}
// 📌 Gửi dữ liệu cập nhật lên hệ thống
await this.handleUpdateBid(data);
async handlePlaceBid() {
if (!this.page_context) {
console.log(
`⚠️ [${this.id}] No page context found, aborting bid process.`
);
return;
}
const page = this.page_context;
console.log('✅ Update successful!');
return { ...response, name, close_time };
};
async getContinueShopButton() {
try {
if (!this.page_context) return null;
await this.page_context.waitForSelector('.btn.btn-block.btn-primary.error.continue-shopping', { timeout: 3000 });
return await this.page_context.evaluate(() => {
const el = document.querySelector('.btn.btn-block.btn-primary.error.continue-shopping');
return el;
});
} catch (error) {
return null;
}
if (global[`IS_PLACE_BID-${this.id}`]) {
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
return;
}
async handlePlaceBid() {
if (!this.page_context) {
console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
try {
console.log(`🔄 [${this.id}] Starting bid process...`);
global[`IS_PLACE_BID-${this.id}`] = true;
const continueShopBtn = await this.getContinueShopButton();
if (continueShopBtn) {
console.log(
`⚠️ [${this.id}] Outbid detected, calling outBid function.`
);
await outBid(this.id);
return;
}
// Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
if (this.current_price > this.max_price + this.plus_price) {
console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
return; // Dừng hàm nếu giá đã vượt qua giới hạn
}
// Kiểm tra thời gian bid
if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
console.log(
`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
this.name || "None"
}`
);
return;
}
// Đợi phản hồi từ API
const response = await this.waitForApiResponse();
// Kiểm tra nếu phản hồi không tồn tại hoặc nếu giá đấu của người dùng bằng với giá tối đa hiện tại
if (
!response ||
(response?.lotData?.myBid &&
response.lotData.myBid == this.max_price) ||
response?.lotData?.minimumBid > this.max_price
) {
console.log(
`⚠️ [${this.id}] No response or myBid equals max_price:`,
response
); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa
return; // Nếu không có phản hồi hoặc giá đấu bằng giá tối đa thì dừng hàm
}
// Kiểm tra nếu dữ liệu trong response có tồn tại và trạng thái đấu giá (bidStatus) không phải là 'None'
if (
response.lotData &&
response.lotData?.bidStatus !== "None" &&
this.max_price == response?.lotData.myBid
) {
console.log(
`✔️ [${this.id}] Bid status is not 'None'. Current bid status:`,
response.lotData?.bidStatus
); // Ghi log nếu trạng thái đấu giá không phải 'None'
return; // Nếu trạng thái đấu giá không phải là 'None', dừng hàm
}
const bidHistoriesItem = _.maxBy(this.histories, "price");
console.log(`📜 [${this.id}] Current bid history:`, this.histories);
if (
bidHistoriesItem &&
bidHistoriesItem?.price === this.current_price &&
this.max_price == response?.lotData.myBid
) {
console.log(
`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`
);
return;
}
console.log(
`💰 [${this.id}] Placing a bid with amount: ${this.reserve_price}`
);
// 📌 Làm rỗng ô input trước khi nhập giá đấu
await page.evaluate(() => {
document.querySelector("#place-bid").value = "";
});
console.log(`📝 [${this.id}] Cleared bid input field.`);
// 📌 Nhập giá đấu vào ô input
await page.type("#place-bid", String(this.max_price), { delay: 800 });
console.log(`✅ [${this.id}] Entered bid amount: ${this.max_price}`);
// 📌 Lấy giá trị thực tế từ ô input sau khi nhập
const bidValue = await page.evaluate(
() => document.querySelector("#place-bid").value
);
console.log(`🔍 Entered bid value: ${bidValue}`);
// 📌 Kiểm tra nếu giá trị nhập vào không khớp với giá trị mong muốn
if (!bidValue || bidValue !== String(this.max_price)) {
console.log(`❌ Incorrect bid amount! Received: ${bidValue}`);
return; // Dừng thực hiện nếu giá trị nhập sai
}
// 📌 Nhấn nút "Place Bid"
await page.click(
".place-bid-submit .btn.btn-primary.btn-block.place-bid-btn",
{ delay: 5000 }
);
console.log(`🖱️ [${this.id}] Clicked "Place Bid" button.`);
console.log(`📩 [${this.id}] Bid submitted, waiting for navigation...`);
// 📌 Chờ trang load lại để cập nhật trạng thái đấu giá
await page.waitForNavigation({
timeout: 8000,
waitUntil: "domcontentloaded",
});
console.log(`🔄 [${this.id}] Page reloaded, checking bid status...`);
const { lotData } = await this.waitForApiResponse();
console.log(`📡 [${this.id}] API Response received:`, lotData);
// 📌 Kiểm tra trạng thái đấu giá từ API
if (lotData?.myBid == this.max_price) {
console.log(`📸 [${this.id}] Taking bid success snapshot...`);
await takeSnapshot(
page,
this,
"bid-success",
CONSTANTS.TYPE_IMAGE.SUCCESS
);
sendMessage(this);
console.log(`✅ [${this.id}] Bid placed successfully!`);
return;
}
console.log(
`⚠️ [${this.id}] Bid action completed, but status is still "None".`
);
} catch (error) {
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
} finally {
console.log(`🔚 [${this.id}] Resetting bid flag.`);
global[`IS_PLACE_BID-${this.id}`] = false;
}
}
async handleCreateLogsOnServer(data) {
const values = data.map((item) => {
return {
model: item.pid,
lot_id: item.lotId,
out_price: item.lotData.minimumBid || 0,
raw_data: JSON.stringify(item),
};
});
await createOutBidLog(values);
}
async gotoLink() {
const page = this.page_context;
if (page.isClosed()) {
console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`);
return;
}
console.log(`🔄 [${this.id}] Starting the bidding process...`);
try {
console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
await page.goto(this.url, { waitUntil: "networkidle2" });
console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
await page.bringToFront();
console.log(`🛠️ [${this.id}] Setting custom user agent...`);
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
);
console.log(`🎯 [${this.id}] Listening for API responses...`);
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
// page.removeAllListeners('request');
// await page.setRequestInterception(true);
// page.on('request', (request) => {
// if (request.url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
// console.log('🚀 Fake response cho request:', request.url());
// const fakeData = fs.readFileSync('./data/fake-out-lot-langtons.json', 'utf8');
// request.respond({
// status: 200,
// contentType: 'application/json',
// body: fakeData,
// });
// } else {
// try {
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
// } catch (error) {
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
// }
// }
// });
const onResponse = async (response) => {
const url = response?.request()?.url();
if (
!url ||
!url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)
) {
return;
}
try {
const { lotData, ...prev } = await response.json();
console.log(`📜 [${this.id}] Received lotData:`, lotData);
if (!lotData || lotData.lotId !== this.lot_id) {
console.log(
`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`
);
await this.page_context.reload({ waitUntil: "networkidle0" });
console.log(`🔁 [${this.id}] Reload page in gotoLink`);
return;
}
const page = this.page_context;
}
if (global[`IS_PLACE_BID-${this.id}`]) {
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
return;
}
console.log(`🔍 [${this.id}] Checking bid status...`);
try {
console.log(`🔄 [${this.id}] Starting bid process...`);
global[`IS_PLACE_BID-${this.id}`] = true;
if (["Outbid"].includes(lotData?.bidStatus)) {
console.log(
`⚠️ [${this.id}] Outbid detected, attempting to place a new bid...`
);
const continueShopBtn = await this.getContinueShopButton();
if (continueShopBtn) {
console.log(`⚠️ [${this.id}] Outbid detected, calling outBid function.`);
await outBid(this.id);
return;
this.handleCreateLogsOnServer([{ lotData, ...prev }]);
} else if (["Winning"].includes(lotData?.bidStatus)) {
const bidHistoriesItem = _.maxBy(this.histories, "price");
if (
!bidHistoriesItem ||
bidHistoriesItem?.price != lotData?.currentMaxBid
) {
pushPrice({
bid_id: this.id,
price: lotData?.currentMaxBid,
});
}
}
// Kiểm tra nếu giá hiện tại lớn hơn giá tối đa cộng thêm giá cộng thêm
if (this.current_price > this.max_price + this.plus_price) {
console.log(`⚠️ [${this.id}] Outbid bid`); // Ghi log cảnh báo nếu giá hiện tại vượt quá mức tối đa cho phép
return; // Dừng hàm nếu giá đã vượt qua giới hạn
}
// Kiểm tra thời gian bid
if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
console.log(`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${this.name || 'None'}`);
return;
}
// Đợi phản hồi từ API
const response = await this.waitForApiResponse();
// Kiểm tra nếu phản hồi không tồn tại hoặc nếu giá đấu của người dùng bằng với giá tối đa hiện tại
if (!response || (response?.lotData?.myBid && response.lotData.myBid == this.max_price) || response?.lotData?.minimumBid > this.max_price) {
console.log(`⚠️ [${this.id}] No response or myBid equals max_price:`, response); // Ghi log nếu không có phản hồi hoặc giá đấu của người dùng bằng giá tối đa
return; // Nếu không có phản hồi hoặc giá đấu bằng giá tối đa thì dừng hàm
}
// Kiểm tra nếu dữ liệu trong response có tồn tại và trạng thái đấu giá (bidStatus) không phải là 'None'
if (response.lotData && response.lotData?.bidStatus !== 'None' && this.max_price == response?.lotData.myBid) {
console.log(`✔️ [${this.id}] Bid status is not 'None'. Current bid status:`, response.lotData?.bidStatus); // Ghi log nếu trạng thái đấu giá không phải 'None'
return; // Nếu trạng thái đấu giá không phải là 'None', dừng hàm
}
const bidHistoriesItem = _.maxBy(this.histories, 'price');
console.log(`📜 [${this.id}] Current bid history:`, this.histories);
if (bidHistoriesItem && bidHistoriesItem?.price === this.current_price && this.max_price == response?.lotData.myBid) {
console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`);
return;
}
console.log(`💰 [${this.id}] Placing a bid with amount: ${this.reserve_price}`);
// 📌 Làm rỗng ô input trước khi nhập giá đấu
await page.evaluate(() => {
document.querySelector('#place-bid').value = '';
});
console.log(`📝 [${this.id}] Cleared bid input field.`);
// 📌 Nhập giá đấu vào ô input
await page.type('#place-bid', String(this.max_price), { delay: 800 });
console.log(`✅ [${this.id}] Entered bid amount: ${this.max_price}`);
// 📌 Lấy giá trị thực tế từ ô input sau khi nhập
const bidValue = await page.evaluate(() => document.querySelector('#place-bid').value);
console.log(`🔍 Entered bid value: ${bidValue}`);
// 📌 Kiểm tra nếu giá trị nhập vào không khớp với giá trị mong muốn
if (!bidValue || bidValue !== String(this.max_price)) {
console.log(`❌ Incorrect bid amount! Received: ${bidValue}`);
return; // Dừng thực hiện nếu giá trị nhập sai
}
// 📌 Nhấn nút "Place Bid"
await page.click('.place-bid-submit .btn.btn-primary.btn-block.place-bid-btn', { delay: 5000 });
console.log(`🖱️ [${this.id}] Clicked "Place Bid" button.`);
console.log(`📩 [${this.id}] Bid submitted, waiting for navigation...`);
// 📌 Chờ trang load lại để cập nhật trạng thái đấu giá
await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' });
console.log(`🔄 [${this.id}] Page reloaded, checking bid status...`);
const { lotData } = await this.waitForApiResponse();
console.log(`📡 [${this.id}] API Response received:`, lotData);
// 📌 Kiểm tra trạng thái đấu giá từ API
if (lotData?.myBid == this.max_price) {
console.log(`📸 [${this.id}] Taking bid success snapshot...`);
await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS);
sendMessage(this);
console.log(`✅ [${this.id}] Bid placed successfully!`);
return;
}
console.log(`⚠️ [${this.id}] Bid action completed, but status is still "None".`);
if (
lotData.myBid &&
this.max_price &&
this.max_price != lotData.myBid
) {
this.handlePlaceBid();
}
} catch (error) {
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
} finally {
console.log(`🔚 [${this.id}] Resetting bid flag.`);
global[`IS_PLACE_BID-${this.id}`] = false;
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
}
};
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
this.page_context.off("response", onResponse);
console.log(`📡 [${this.id}] Attaching new response listener...`);
this.page_context.on("response", onResponse);
console.log(`✅ [${this.id}] Navigation setup complete.`);
} catch (error) {
console.error(`❌ [${this.id}] Error during navigation:`, error);
}
}
async handleCreateLogsOnServer(data) {
const values = data.map((item) => {
return {
model: item.pid,
lot_id: item.lotId,
out_price: item.lotData.minimumBid || 0,
raw_data: JSON.stringify(item),
};
});
action = async () => {
try {
const page = this.page_context;
await createOutBidLog(values);
// 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
if (!page.url() || !page.url().includes(this.url)) {
console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
await this.gotoLink();
}
await this.handlePlaceBid();
} catch (error) {
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
}
async gotoLink() {
const page = this.page_context;
if (page.isClosed()) {
console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`);
return;
}
console.log(`🔄 [${this.id}] Starting the bidding process...`);
try {
console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
await page.goto(this.url, { waitUntil: 'networkidle2' });
console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
await page.bringToFront();
console.log(`🛠️ [${this.id}] Setting custom user agent...`);
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
console.log(`🎯 [${this.id}] Listening for API responses...`);
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới
// page.removeAllListeners('request');
// await page.setRequestInterception(true);
// page.on('request', (request) => {
// if (request.url().includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
// console.log('🚀 Fake response cho request:', request.url());
// const fakeData = fs.readFileSync('./data/fake-out-lot-langtons.json', 'utf8');
// request.respond({
// status: 200,
// contentType: 'application/json',
// body: fakeData,
// });
// } else {
// try {
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
// } catch (error) {
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
// }
// }
// });
const onResponse = async (response) => {
const url = response?.request()?.url();
if (!url || !url.includes(configs.WEB_CONFIGS.LANGTONS.API_CALL_TO_TRACKING)) {
return;
}
try {
const { lotData, ...prev } = await response.json();
console.log(`📜 [${this.id}] Received lotData:`, lotData);
if (!lotData || lotData.lotId !== this.lot_id) {
console.log(`⚠️ [${this.id}] Ignored response for lotId: ${lotData?.lotId}`);
await this.page_context.reload({ waitUntil: 'networkidle0' });
console.log(`🔁 [${this.id}] Reload page in gotoLink`);
return;
}
console.log(`🔍 [${this.id}] Checking bid status...`);
if (['Outbid'].includes(lotData?.bidStatus)) {
console.log(`⚠️ [${this.id}] Outbid detected, attempting to place a new bid...`);
this.handleCreateLogsOnServer([{ lotData, ...prev }]);
} else if (['Winning'].includes(lotData?.bidStatus)) {
const bidHistoriesItem = _.maxBy(this.histories, 'price');
if (!bidHistoriesItem || bidHistoriesItem?.price != lotData?.currentMaxBid) {
pushPrice({
bid_id: this.id,
price: lotData?.currentMaxBid,
});
}
}
if (lotData.myBid && this.max_price && this.max_price != lotData.myBid) {
this.handlePlaceBid();
}
} catch (error) {
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
}
};
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
this.page_context.off('response', onResponse);
console.log(`📡 [${this.id}] Attaching new response listener...`);
this.page_context.on('response', onResponse);
console.log(`✅ [${this.id}] Navigation setup complete.`);
} catch (error) {
console.error(`❌ [${this.id}] Error during navigation:`, error);
}
}
action = async () => {
try {
const page = this.page_context;
// 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
if (!page.url() || !page.url().includes(this.url)) {
console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
await this.gotoLink();
}
await this.handlePlaceBid();
} catch (error) {
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
}
};
};
}

View File

@ -1,367 +1,438 @@
import _ from 'lodash';
import { pushPrice, updateBid } from '../../system/apis/bid.js';
import { sendMessage } from '../../system/apis/notification.js';
import configs from '../../system/config.js';
import { delay, extractPriceNumber, isTimeReached, removeFalsyValues } from '../../system/utils.js';
import { ProductBid } from '../product-bid.js';
import _ from "lodash";
import { pushPrice, updateBid } from "../../system/apis/bid.js";
import { sendMessage } from "../../system/apis/notification.js";
import configs from "../../system/config.js";
import {
delay,
extractPriceNumber,
isTimeReached,
removeFalsyValues,
} from "../../system/utils.js";
import { ProductBid } from "../product-bid.js";
export class LawsonsProductBid extends ProductBid {
constructor({ ...prev }) {
super(prev);
constructor({ ...prev }) {
super(prev);
}
async handleUpdateBid({
lot_id,
close_time,
name,
current_price,
reserve_price,
}) {
const response = await updateBid(this.id, {
lot_id,
close_time,
name,
current_price,
reserve_price: Number(reserve_price) || 0,
});
if (response) {
this.lot_id = response.lot_id;
this.close_time = response.close_time;
this.start_bid_time = response.start_bid_time;
}
}
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) {
const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 });
async getReversePrice() {
try {
if (!this.page_context) return null;
if (response) {
this.lot_id = response.lot_id;
this.close_time = response.close_time;
this.start_bid_time = response.start_bid_time;
}
await this.page_context.waitForSelector(
".select-dropdown-value.text-truncate",
{ timeout: 4000 }
);
const price = await this.page_context.evaluate(() => {
const el = document.querySelector(
".select-dropdown-value.text-truncate"
);
return el ? el.innerText : null;
});
return price ? extractPriceNumber(price) : null;
} catch (error) {
console.log(error.message);
return null;
}
}
async getReversePrice() {
try {
if (!this.page_context) return null;
update = async () => {
try {
if (!this.page_context) return;
await this.page_context.waitForSelector('.select-dropdown-value.text-truncate', { timeout: 4000 });
const price = await this.page_context.evaluate(() => {
const el = document.querySelector('.select-dropdown-value.text-truncate');
return el ? el.innerText : null;
// if (this.updated_at) {
// await this.page_context.reload({ waitUntil: 'networkidle0' });
// }
const result = await this.waitApiInfo();
const reservePrice = await this.getReversePrice();
console.log({ reservePrice });
if (!result) return;
// 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
const data = removeFalsyValues(
{
lot_id: String(result?.itemView.lotId) || null,
reserve_price: reservePrice,
current_price: result?.currentBidAmount || null,
close_time: new Date(result.endTime).toUTCString() || null,
// close_time: this.close_time ? null : new Date(Date.now() + 5 * 60 * 1000).toUTCString(), //test
name: result?.itemView?.title || null,
},
// [],
["close_time"]
);
console.log(`🚀 [${this.id}] Processed data ready for update`);
// 📌 Gửi dữ liệu cập nhật lên hệ thống
await this.handleUpdateBid(data);
} catch (error) {
console.log("Error Update", error.message);
}
};
// Hàm con để fetch trong context trình duyệt
fetchFromPage = async (url) => {
return await this.page_context.evaluate(async (url) => {
try {
const res = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
return await res.json();
} catch (err) {
return { error: err.message };
}
}, url);
};
submitBid() {
return new Promise(async (resolve, reject) => {
if (!this.page_context || !this.model) {
console.log(`[${this.id}] Page context or model is missing.`);
reject(null);
return;
}
try {
console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
const result = await this.page_context.evaluate(
async (bidAmount, lotRef, url) => {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
bidAmount,
lotRef,
v2: true,
}),
});
return price ? extractPriceNumber(price) : null;
} catch (error) {
console.log(error.message);
return null;
}
}
update = async () => {
try {
if (!this.page_context) return;
// if (this.updated_at) {
// await this.page_context.reload({ waitUntil: 'networkidle0' });
// }
const result = await this.waitApiInfo();
const reservePrice = await this.getReversePrice();
console.log({ reservePrice });
if (!result) return;
// 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
const data = removeFalsyValues(
{
lot_id: String(result?.itemView.lotId) || null,
reserve_price: reservePrice,
current_price: result?.currentBidAmount || null,
close_time: new Date(result.endTime).toUTCString() || null,
// close_time: this.close_time ? null : new Date(Date.now() + 5 * 60 * 1000).toUTCString(), //test
name: result?.itemView?.title || null,
},
// [],
['close_time'],
);
console.log(`🚀 [${this.id}] Processed data ready for update`);
// 📌 Gửi dữ liệu cập nhật lên hệ thống
await this.handleUpdateBid(data);
} catch (error) {
console.log('Error Update', error.message);
}
};
// Hàm con để fetch trong context trình duyệt
fetchFromPage = async (url) => {
return await this.page_context.evaluate(async (url) => {
try {
const res = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
return await res.json();
} catch (err) {
return { error: err.message };
}
}, url);
};
submitBid() {
return new Promise(async (resolve, reject) => {
if (!this.page_context || !this.model) {
console.log(`[${this.id}] Page context or model is missing.`);
reject(null);
return;
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
try {
console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
return await response.json();
},
this.max_price,
this.model,
configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT
);
const result = await this.page_context.evaluate(
async (bidAmount, lotRef, url) => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
bidAmount,
lotRef,
v2: true,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
},
this.max_price,
this.model,
configs.WEB_CONFIGS.LAWSONS.API_CHECKOUT,
);
console.log('🧾 API Bid Result:', {
bid_amount: this.max_price,
result,
});
if (!result?.data?.orderBidResponse?.success) reject(null);
resolve(result);
} catch (err) {
console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
reject(null);
}
console.log("🧾 API Bid Result:", {
bid_amount: this.max_price,
result,
});
if (!result?.data?.orderBidResponse?.success) reject(null);
resolve(result);
} catch (err) {
console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
reject(null);
}
});
}
async handlePlaceBid() {
// Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá
if (!this.page_context) {
console.log(
`⚠️ [${this.id}] No page context found, aborting bid process.`
);
return;
}
const page = this.page_context;
// Kiểm tra xem đấu giá đã đang diễn ra chưa. Nếu có thì không thực hiện nữa
if (global[`IS_PLACE_BID-${this.id}`]) {
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
return;
}
async handlePlaceBid() {
// Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá
if (!this.page_context) {
console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`);
return;
}
const page = this.page_context;
try {
console.log(`🔄 [${this.id}] Starting bid process...`);
// Đánh dấu rằng đang thực hiện quá trình đấu giá để tránh đấu lại
global[`IS_PLACE_BID-${this.id}`] = true;
// Kiểm tra xem đấu giá đã đang diễn ra chưa. Nếu có thì không thực hiện nữa
if (global[`IS_PLACE_BID-${this.id}`]) {
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
return;
// Kiểm tra xem giá hiện tại có vượt qua mức giá tối đa chưa
if (this.current_price > this.max_price + this.plus_price) {
console.log(`⚠️ [${this.id}] Outbid bid`);
return; // Nếu giá hiện tại vượt quá mức giá tối đa thì dừng lại
}
// Kiểm tra thời gian đấu giá
if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
console.log(
`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
this.name || "None"
}`
);
return; // Nếu chưa đến giờ đấu giá thì bỏ qua
}
// Đợi lấy thông tin API để kiểm tra tình trạng đấu giá hiện tại
const response = await this.waitApiInfo();
// Lấy giá reserve price để kiểm tra
const reservePrice = await this.getReversePrice();
// Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá
const shouldStop =
!response ||
response?.currentBidAmount > this.max_price + this.plus_price ||
response.isOutBid != true ||
!reservePrice ||
reservePrice > this.max_price + this.plus_price;
if (shouldStop) {
console.log(`⚠️ [${this.id}] Stop bidding:`, {
reservePrice,
currentBidAmount: response?.currentBidAmount,
maxBidAmount: response?.maxBidAmount,
});
return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá
}
// Tìm bid history lớn nhất từ các lịch sử đấu giá của item
const bidHistoriesItem = _.maxBy(this.histories, "price");
console.log(`📜 [${this.id}] Current bid history:`, this.histories);
// Kiểm tra xem đã bid rồi chưa. Nếu đã bid rồi thì bỏ qua
if (
bidHistoriesItem &&
bidHistoriesItem?.price == this.current_price &&
this.max_price + this.plus_price == response?.maxBidAmount
) {
console.log(
`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})`
);
return;
}
if (this.reserve_price <= 0) {
console.log(`[${this.reserve_price}]`);
return;
}
console.log(
`===============Start call to submit [${this.id}] ================`
);
await delay(2000);
// Nếu chưa bid, thực hiện đặt giá
console.log(
`💰 [${this.id}] Placing a bid with amount: ${this.max_price}`
);
// Gửi bid qua API và nhận kết quả
const result = await this.submitBid();
// Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại
if (!result) return;
console.log({ result });
// Gửi thông báo đã đấu giá thành công
sendMessage(this);
await this.page_context.reload({ waitUntil: "networkidle0" });
console.log(`✅ [${this.id}] Bid placed successfully!`);
} catch (error) {
// Nếu có lỗi xảy ra trong quá trình đấu giá, log lại lỗi
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
} finally {
// Đảm bảo luôn reset trạng thái đấu giá sau khi hoàn thành
console.log(`🔚 [${this.id}] Resetting bid flag.`);
global[`IS_PLACE_BID-${this.id}`] = false;
}
}
async waitApiInfo() {
if (!this.page_context) {
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
return null;
}
const infoUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model);
const detailUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_PRODUCT(
this.model
);
const [info, detailData] = await Promise.all([
this.fetchFromPage(infoUrl),
this.fetchFromPage(detailUrl),
]);
return { ...info, ...detailData };
}
async trackingOutbid() {
if (!this.page_context) return;
try {
const onResponse = async (response) => {
const url = response?.request()?.url();
if (
!url ||
!url.includes(configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model))
) {
return;
}
try {
console.log(`🔄 [${this.id}] Starting bid process...`);
// Đánh dấu rằng đang thực hiện quá trình đấu giá để tránh đấu lại
global[`IS_PLACE_BID-${this.id}`] = true;
const result = await response.json();
// Kiểm tra xem giá hiện tại có vượt qua mức giá tối đa chưa
if (this.current_price > this.max_price + this.plus_price) {
console.log(`⚠️ [${this.id}] Outbid bid`);
return; // Nếu giá hiện tại vượt quá mức giá tối đa thì dừng lại
}
if (!result) return;
// Kiểm tra thời gian đấu giá
if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
console.log(`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${this.name || 'None'}`);
return; // Nếu chưa đến giờ đấu giá thì bỏ qua
}
console.log(`📈 [${this.id}] Bid data: `, result);
// Đợi lấy thông tin API để kiểm tra tình trạng đấu giá hiện tại
const response = await this.waitApiInfo();
const { maxBidAmount, currentBidAmount, isOutBid } = result;
// Lấy giá reserve price để kiểm tra
const reservePrice = await this.getReversePrice();
console.log(
`📊 [${this.id}] API Info - maxBidAmount: ${maxBidAmount}, currentBidAmount: ${currentBidAmount}, isOutBid: ${isOutBid}`
);
// Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá
const shouldStop =
!response ||
response?.currentBidAmount > this.max_price + this.plus_price ||
response.isOutBid != true ||
!reservePrice ||
reservePrice > this.max_price + this.plus_price;
// Lấy giá reverse (giá thấp nhất cần để thắng đấu giá)
const reversePrice = await this.getReversePrice();
console.log(`💰 [${this.id}] Current reverse price: ${reversePrice}`);
if (shouldStop) {
console.log(`⚠️ [${this.id}] Stop bidding:`, { reservePrice, currentBidAmount: response?.currentBidAmount, maxBidAmount: response?.maxBidAmount });
return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá
}
// Tìm ra lịch sử đấu giá có giá cao nhất trong this.histories
const bidHistoriesItem = _.maxBy(this.histories, "price");
console.log(
`📈 [${this.id}] Highest local bid: ${
bidHistoriesItem?.price ?? "N/A"
}`
);
// Tìm bid history lớn nhất từ các lịch sử đấu giá của item
const bidHistoriesItem = _.maxBy(this.histories, 'price');
console.log(`📜 [${this.id}] Current bid history:`, this.histories);
if (!this.close_time || !this.lot_id || !this.current_price) return;
// Kiểm tra xem đã bid rồi chưa. Nếu đã bid rồi thì bỏ qua
if (bidHistoriesItem && bidHistoriesItem?.price == this.current_price && this.max_price + this.plus_price == response?.maxBidAmount) {
console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})`);
return;
}
if (this.reserve_price <= 0) {
console.log(`[${this.reserve_price}]`);
return;
}
console.log(`===============Start call to submit [${this.id}] ================`);
await delay(20000);
// Nếu chưa bid, thực hiện đặt giá
console.log(`💰 [${this.id}] Placing a bid with amount: ${this.max_price}`);
// Gửi bid qua API và nhận kết quả
const result = await this.submitBid();
// Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại
if (!result) return;
console.log({ result });
// Gửi thông báo đã đấu giá thành công
sendMessage(this);
await this.page_context.reload({ waitUntil: 'networkidle0' });
console.log(`✅ [${this.id}] Bid placed successfully!`);
} catch (error) {
// Nếu có lỗi xảy ra trong quá trình đấu giá, log lại lỗi
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
} finally {
// Đảm bảo luôn reset trạng thái đấu giá sau khi hoàn thành
console.log(`🔚 [${this.id}] Resetting bid flag.`);
global[`IS_PLACE_BID-${this.id}`] = false;
}
}
async waitApiInfo() {
if (!this.page_context) {
console.error(`❌ [${this.id}] Error: page_context is undefined.`);
return null;
}
const infoUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model);
const detailUrl = configs.WEB_CONFIGS.LAWSONS.API_DETAIL_PRODUCT(this.model);
const [info, detailData] = await Promise.all([this.fetchFromPage(infoUrl), this.fetchFromPage(detailUrl)]);
return { ...info, ...detailData };
}
async trackingOutbid() {
if (!this.page_context) return;
try {
const onResponse = async (response) => {
const url = response?.request()?.url();
if (!url || !url.includes(configs.WEB_CONFIGS.LAWSONS.API_DETAIL_INFO(this.model))) {
return;
}
try {
const result = await response.json();
if (!result) return;
console.log(`📈 [${this.id}] Bid data: `, result);
const { maxBidAmount, currentBidAmount, isOutBid } = result;
console.log(`📊 [${this.id}] API Info - maxBidAmount: ${maxBidAmount}, currentBidAmount: ${currentBidAmount}, isOutBid: ${isOutBid}`);
// Lấy giá reverse (giá thấp nhất cần để thắng đấu giá)
const reversePrice = await this.getReversePrice();
console.log(`💰 [${this.id}] Current reverse price: ${reversePrice}`);
// Tìm ra lịch sử đấu giá có giá cao nhất trong this.histories
const bidHistoriesItem = _.maxBy(this.histories, 'price');
console.log(`📈 [${this.id}] Highest local bid: ${bidHistoriesItem?.price ?? 'N/A'}`);
if (!this.close_time || !this.lot_id || !this.current_price) return;
// Nếu chưa từng đặt giá và có giá tối đa (maxBidAmount), thì push giá đó vào histories
if ((!bidHistoriesItem && maxBidAmount) || (bidHistoriesItem?.price != currentBidAmount && currentBidAmount == maxBidAmount)) {
console.log(`🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.`);
pushPrice({
bid_id: this.id,
price: currentBidAmount,
});
}
// Nếu giá hiện tại cao hơn giá mình đã đặt, và reversePrice vẫn trong giới hạn cho phép, và đang bị outbid thì sẽ đặt giá tiếp
if (reversePrice <= this.max_price + this.plus_price && isOutBid && currentBidAmount <= this.max_price + this.plus_price && this.max_price != maxBidAmount) {
console.log(`⚠️ [${this.id}] Outbid detected. Reverse price acceptable. Placing a new bid...`);
await this.handlePlaceBid();
} else {
console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
}
if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) {
await this.page_context.reload({ waitUntil: 'networkidle0' });
}
} catch (error) {
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
}
};
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
this.page_context.off('response', onResponse);
console.log(`📡 [${this.id}] Attaching new response listener...`);
this.page_context.on('response', onResponse);
console.log(`✅ [${this.id}] Navigation setup complete.`);
} catch (error) {
console.error(`❌ [${this.id}] Error during navigation:`, error);
}
}
async gotoLink() {
const page = this.page_context;
if (page.isClosed()) {
console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`);
return;
}
console.log(`🔄 [${this.id}] Starting the bidding process...`);
try {
console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
await page.goto(this.url, { waitUntil: 'networkidle2' });
console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
await page.bringToFront();
console.log(`🛠️ [${this.id}] Setting custom user agent...`);
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
console.log(`🎯 [${this.id}] Listening for API responses...`);
// tracking out bid
this.trackingOutbid();
} catch (error) {
console.error(`❌ [${this.id}] Error during navigation:`, error);
}
}
action = async () => {
try {
const page = this.page_context;
// 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
if (!page.url() || !page.url().includes(this.url)) {
console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
await this.gotoLink();
}
// Nếu chưa từng đặt giá và có giá tối đa (maxBidAmount), thì push giá đó vào histories
if (
(!bidHistoriesItem && maxBidAmount) ||
(bidHistoriesItem?.price != currentBidAmount &&
currentBidAmount == maxBidAmount)
) {
console.log(
`🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.`
);
pushPrice({
bid_id: this.id,
price: currentBidAmount,
});
}
// Nếu giá hiện tại cao hơn giá mình đã đặt, và reversePrice vẫn trong giới hạn cho phép, và đang bị outbid thì sẽ đặt giá tiếp
if (
reversePrice <= this.max_price + this.plus_price &&
isOutBid &&
currentBidAmount <= this.max_price + this.plus_price &&
this.max_price != maxBidAmount
) {
console.log(
`⚠️ [${this.id}] Outbid detected. Reverse price acceptable. Placing a new bid...`
);
await this.handlePlaceBid();
} else {
console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
}
if (new Date(this.updated_at).getTime() > Date.now() - 120 * 1000) {
await this.page_context.reload({ waitUntil: "networkidle0" });
}
} catch (error) {
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
}
};
};
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
this.page_context.off("response", onResponse);
console.log(`📡 [${this.id}] Attaching new response listener...`);
this.page_context.on("response", onResponse);
console.log(`✅ [${this.id}] Navigation setup complete.`);
} catch (error) {
console.error(`❌ [${this.id}] Error during navigation:`, error);
}
}
async gotoLink() {
const page = this.page_context;
if (page.isClosed()) {
console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`);
return;
}
console.log(`🔄 [${this.id}] Starting the bidding process...`);
try {
console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
await page.goto(this.url, { waitUntil: "networkidle2" });
console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
await page.bringToFront();
console.log(`🛠️ [${this.id}] Setting custom user agent...`);
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
);
console.log(`🎯 [${this.id}] Listening for API responses...`);
// tracking out bid
this.trackingOutbid();
} catch (error) {
console.error(`❌ [${this.id}] Error during navigation:`, error);
}
}
action = async () => {
try {
const page = this.page_context;
// 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
if (!page.url() || !page.url().includes(this.url)) {
console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
await this.gotoLink();
}
await this.handlePlaceBid();
} catch (error) {
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
}
};
}

View File

@ -0,0 +1,151 @@
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 PicklesApiBid extends ApiBid {
reloadInterval = null;
constructor({ ...prev }) {
super(prev);
}
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.PICKLES.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);
}
};
isLogin = async () => {
if (!this.page_context) return false;
const filePath = getPathProfile(this.origin_url);
return (
!(await this.page_context.$('[name="_58_login"]')) &&
fs.existsSync(filePath)
);
};
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.$('[name="_58_login"]')) && 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('[name="_58_login"]', this.username, { delay: 100 });
// ⌨ Enter password
console.log(`✍ [${this.id}] Entering password...`);
await page.type('[name="_58_password"]', this.password, { delay: 150 });
// 🚀 Click the login button
console.log(`🔘 [${this.id}] Clicking the "Login" button`);
await page.click("#sign-in-btn", { delay: 92 });
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;
}
}
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
};
}

View File

@ -0,0 +1,479 @@
import _ from "lodash";
import { pushPrice, updateBid } from "../../system/apis/bid.js";
import configs from "../../system/config.js";
import { delay, isTimeReached, removeFalsyValues } from "../../system/utils.js";
import { ProductBid } from "../product-bid.js";
import { sendMessage } from "../../system/apis/notification.js";
export class PicklesProductBid extends ProductBid {
constructor({ ...prev }) {
super(prev);
}
async handleUpdateBid({
lot_id,
close_time,
name,
current_price,
reserve_price,
}) {
const response = await updateBid(this.id, {
lot_id,
close_time,
name,
current_price,
reserve_price: Number(reserve_price) || 0,
});
if (response) {
this.lot_id = response.lot_id;
this.close_time = response.close_time;
this.start_bid_time = response.start_bid_time;
}
}
fetchFromPage = async (url) => {
return await this.page_context.evaluate(async (url) => {
try {
const res = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return await res.json();
} catch (err) {
return { error: err.message };
}
}, url);
};
detailData = async () => {
return await this.fetchFromPage(
configs.WEB_CONFIGS.PICKLES.API_DETAIL_PRODUCT(this.model)
);
};
getName = async () => {
if (!this.page_context) return null;
try {
return await this.page_context.$eval(
"#pd-ph-header > div:first-child > div > div:nth-child(2) > div > h1",
(el) => el.textContent
);
} catch (error) {
console.log(
"%cmodels/pickles.com.au/pickles-product-bid.js:60 error.message",
"color: #007acc;",
error.message
);
return null;
}
};
update = async () => {
try {
if (!this.page_context) return;
const result = await this.detailData();
if (!result || !result[0]) return;
const { item, bidding } = result[0];
const name = await this.getName();
console.log(
"%cmodels/pickles.com.au/pickles-product-bid.js:83 item",
"color: #007acc;",
bidding
);
// 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết
const data = removeFalsyValues(
{
lot_id: String(item?.id) || null,
reserve_price: bidding?.minimumBidAmount || null,
current_price: bidding?.currentActualBid || null,
close_time: new Date(item?.itemBidEndTimestamp).toUTCString() || null,
name: name || null,
},
["close_time"]
);
console.log(`🚀 [${this.id}] Processed data ready for update`);
// 📌 Gửi dữ liệu cập nhật lên hệ thống
await this.handleUpdateBid(data);
await this.page_context.reload({ waitUntil: "networkidle2" });
await this.page_context.waitForNavigation({
timeout: 8000,
waitUntil: "domcontentloaded",
});
} catch (error) {
console.log("Error Update", error.message);
}
};
submitBid() {
return new Promise(async (resolve, reject) => {
if (!this.page_context || !this.lot_id) {
console.log(`[${this.id}] Page context or model is missing.`);
reject("Context is not define");
return;
}
try {
console.log(`💰 [${this.id}] Prepared Bid Amount: ${this.max_price}`);
const result = await this.page_context.evaluate(
async (bidAmount, lotRef, url) => {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
itemId: lotRef,
bidValues: {
activity: "BID",
maxBid: bidAmount, // giá trị tối đa của sản phẩm
roundedMaxBid: bidAmount, // giá trị tối đa của sản phẩm
submittedBuyNowValue: null,
},
buyerFeeCalculated: false,
buyerFees: null,
dashboardRedirectUrl: null,
itemTitle: null,
productLine: null,
registrationRequired: false,
totalAmount: null,
updateDetailsRequired: null,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
},
this.max_price + this.plus_price,
this.lot_id,
configs.WEB_CONFIGS.PICKLES.API_CHECKOUT
);
console.log("🧾 API Bid Result:", {
bid_amount: this.max_price + this.plus_price,
result,
});
if (!result?.confirmationRequest) reject("Api call failure");
resolve(result);
} catch (err) {
console.log(`[${this.id}] Failed to submit bid: ${err.message}`);
reject(err);
}
});
}
async handlePlaceBid() {
// Kiểm tra xem có page context không, nếu không có thì kết thúc quá trình đấu giá
if (!this.page_context) {
console.log(
`⚠️ [${this.id}] No page context found, aborting bid process.`
);
return;
}
const page = this.page_context;
// Kiểm tra xem đấu giá đã đang diễn ra chưa. Nếu có thì không thực hiện nữa
if (global[`IS_PLACE_BID-${this.id}`]) {
console.log(`⚠️ [${this.id}] Bid is already in progress, skipping.`);
return;
}
try {
console.log(`🔄 [${this.id}] Starting bid process...`);
// Đánh dấu rằng đang thực hiện quá trình đấu giá để tránh đấu lại
global[`IS_PLACE_BID-${this.id}`] = true;
// Kiểm tra xem giá hiện tại có vượt qua mức giá tối đa chưa
if (this.current_price > this.max_price + this.plus_price) {
console.log(`⚠️ [${this.id}] Outbid bid`);
return; // Nếu giá hiện tại vượt quá mức giá tối đa thì dừng lại
}
// Kiểm tra thời gian đấu giá
if (this.start_bid_time && !isTimeReached(this.start_bid_time)) {
console.log(
`⏳ [${this.id}] Not yet time to bid. Skipping Product: ${
this.name || "None"
}`
);
return; // Nếu chưa đến giờ đấu giá thì bỏ qua
}
// Đợi lấy thông tin API để kiểm tra tình trạng đấu giá hiện tại
const response = await this.detailData();
if (!response) {
console.log(`[${this.id}] Can't get info data`);
return;
}
const { bidding } = response[0];
console.log(
"%cmodels/pickles.com.au/pickles-product-bid.js:157 response",
"color: #007acc;",
bidding
);
// // Kiểm tra nếu có lý do nào khiến không thể tiếp tục đấu giá
const shouldStop =
!response ||
bidding?.currentActualBid > this.max_price + this.plus_price ||
(bidding?.userItemBidStatus &&
bidding?.userItemBidStatus?.type !== "OUTBID") ||
!bidding?.minimumBidAmount ||
bidding.minimumBidAmount > this.max_price + this.plus_price;
if (shouldStop) {
console.log(`⚠️ [${this.id}] Stop bidding:`, {
reservePrice: bidding?.minimumBidAmount,
currentBidAmount: response?.currentBidAmount,
maxBidAmount: response?.maxBidAmount,
});
return; // Nếu gặp điều kiện dừng thì không thực hiện đấu giá
}
// Tìm bid history lớn nhất từ các lịch sử đấu giá của item
const bidHistoriesItem = _.maxBy(this.histories, "price");
console.log(`📜 [${this.id}] Current bid history:`, this.histories);
// Kiểm tra xem đã bid rồi chưa. Nếu đã bid rồi thì bỏ qua
if (
bidHistoriesItem &&
bidHistoriesItem?.price == this.current_price &&
this.max_price + this.plus_price == response?.maxBidAmount
) {
console.log(
`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem?.price})`
);
return;
}
if (this.reserve_price <= 0) {
console.log(`[${this.reserve_price}]`);
return;
}
console.log(
`===============Start call to submit [${this.id}] ================`
);
// waiting 2s
await delay(2000);
// Nếu chưa bid, thực hiện đặt giá
console.log(
`💰 [${this.id}] Placing a bid with amount: ${this.max_price}`
);
// Gửi bid qua API và nhận kết quả
const result = await this.submitBid();
// Nếu không có kết quả (lỗi khi gửi bid) thì dừng lại
if (!result || !result?.confirmationRequest) {
console.log(
"%cmodels/pickles.com.au/pickles-product-bid.js:289 Error when call plance bid",
"color: #007acc;",
"Error when call plance bid"
);
return;
}
console.log({ result });
// Gửi thông báo đã đấu giá thành công
// sendMessage(this);
pushPrice({
bid_id: this.id,
price: result?.yourBid || 0,
});
await this.page_context.evaluate(() => location.reload());
console.log(`✅ [${this.id}] Bid placed successfully!`);
} catch (error) {
// Nếu có lỗi xảy ra trong quá trình đấu giá, log lại lỗi
console.log(`🚨 [${this.id}] Error placing bid: ${error.message}`);
} finally {
// Đảm bảo luôn reset trạng thái đấu giá sau khi hoàn thành
console.log(`🔚 [${this.id}] Resetting bid flag.`);
global[`IS_PLACE_BID-${this.id}`] = false;
}
}
isOutBid = async () => {
try {
// Chờ tối đa 10s cho element xuất hiện
const element = await page.waitForSelector(
'[data-testid="pd-pbsb-bit-status-banner"]',
{
timeout: 10000, // 10s
visible: true, // chỉ accept khi element hiện ra
}
);
if (!element) return false; // không có thì return false
// Lấy innerHTML của element
const innerHTML = await page.evaluate((el) => el.innerHTML, element);
console.log(
"%cmodels/pickles.com.au/pickles-product-bid.js:339 {in}",
"color: #007acc;",
{ innerHTML }
);
// Kiểm tra có từ "outbid" không
return innerHTML.includes("outbid");
} catch (error) {
// Nếu lỗi (timeout hoặc gì đó) => return false
return false;
}
};
async trackingOutbid() {
if (!this.page_context) return;
if (global[`TRACKING_PROCRESS_${this.id}`]) {
console.log(`🔄 [${this.id}] Removing previous response listeners...`);
clearInterval(global[`TRACKING_PROCRESS_${this.id}`]);
}
try {
global[`TRACKING_PROCRESS_${this.id}`] = setInterval(async () => {
try {
const result = await this.detailData();
if (!result) return;
console.log(`📈 [${this.id}] Bid data: `, result);
const { item, bidding } = result[0];
console.log(
`📊 [${this.id}] API Info - minimumBidAmount: ${bidding?.minimumBidAmount}, currentActualBid: ${bidding?.currentActualBid}`
);
// Lấy giá reverse (giá thấp nhất cần để thắng đấu giá)
const reversePrice = bidding?.currentActualBid;
const currentBidAmount = bidding?.currentActualBid;
const maxBidAmount = bidding?.buyerCurrentBid?.maximumBid;
console.log(`💰 [${this.id}] Current reverse price: ${reversePrice}`);
// Tìm ra lịch sử đấu giá có giá cao nhất trong this.histories
const bidHistoriesItem = _.maxBy(this.histories, "price");
console.log(
`📈 [${this.id}] Highest local bid: ${
bidHistoriesItem?.price ?? "N/A"
}`
);
if (!this.close_time || !this.lot_id || !this.current_price) return;
// Nếu chưa từng đặt giá và có giá tối đa (maxBidAmount), thì push giá đó vào histories
if (
(!bidHistoriesItem && maxBidAmount) ||
(bidHistoriesItem?.price != currentBidAmount &&
currentBidAmount == maxBidAmount)
) {
console.log(
`🆕 [${this.id}] No previous bid found. Placing initial bid at ${maxBidAmount}.`
);
pushPrice({
bid_id: this.id,
price: Number(currentBidAmount),
});
}
// Nếu giá hiện tại cao hơn giá mình đã đặt, và reversePrice vẫn trong giới hạn cho phép, và đang bị outbid thì sẽ đặt giá tiếp
if (
reversePrice <= this.max_price + this.plus_price &&
currentBidAmount <= this.max_price + this.plus_price &&
this.max_price != maxBidAmount &&
this.histories.length > 0 &&
bidding?.currentMaximumBid !== this.max_price + this.plus_price
) {
console.log(
`⚠️ [${this.id}] Outbid detected. Reverse price acceptable. Placing a new bid...`
);
await this.handlePlaceBid();
} else {
console.log(`✅ [${this.id}] No bid needed. Conditions not met.`);
}
} catch (error) {
console.error(`🚨 [${this.id}] Error parsing API response:`, error);
}
}, 5000);
console.log(`✅ [${this.id}] Navigation setup complete.`);
} catch (error) {
console.error(`❌ [${this.id}] Error during navigationnnnn:`, error);
}
}
async gotoLink() {
const page = this.page_context;
if (page.isClosed()) {
console.error(`❌ [${this.id}] Page has been closed, cannot navigate.`);
return;
}
console.log(`🔄 [${this.id}] Starting the bidding process...`);
try {
console.log(`🌐 [${this.id}] Navigating to: ${this.url} ...`);
await page.goto(this.url, { waitUntil: "networkidle2" });
console.log(`✅ [${this.id}] Successfully navigated to: ${this.url}`);
console.log(`🖥️ [${this.id}] Bringing tab to the foreground...`);
await page.bringToFront();
console.log(`🛠️ [${this.id}] Setting custom user agent...`);
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
);
console.log(`🎯 [${this.id}] Listening for API responses...`);
// tracking out bid
this.trackingOutbid();
} catch (error) {
console.error(`❌ [${this.id}] Error during navigation:`, error);
}
}
action = async () => {
try {
const page = this.page_context;
// 📌 Kiểm tra nếu trang chưa tải đúng URL thì điều hướng đến URL mục tiêu
if (!page.url() || !page.url().includes(this.url)) {
console.log(`🔄 [${this.id}] Navigating to target URL: ${this.url}`);
await this.gotoLink();
}
await this.handlePlaceBid();
} catch (error) {
console.error(`🚨 [${this.id}] Error navigating the page: ${error}`);
}
};
}

View File

@ -1,159 +1,161 @@
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 { delay, getPathProfile, sanitizeFileName } from '../system/utils.js';
import { Bid } from './bid.js';
import * as fs from "fs";
import BID_TYPE from "../system/bid-type.js";
import browser from "../system/browser.js";
import { getPathProfile } from "../system/utils.js";
import { Bid } from "./bid.js";
export class ProductBid extends Bid {
id;
max_price;
model;
lot_id;
plus_price;
close_time;
first_bid;
quantity;
created_at;
updated_at;
histories;
start_bid_time;
parent_browser_context;
web_bid;
current_price;
name;
reserve_price;
update;
id;
max_price;
model;
lot_id;
plus_price;
close_time;
first_bid;
quantity;
created_at;
updated_at;
histories;
start_bid_time;
parent_browser_context;
web_bid;
current_price;
name;
reserve_price;
update;
constructor({
url,
max_price,
plus_price,
model,
first_bid = false,
id,
created_at,
updated_at,
quantity = 1,
histories = [],
close_time,
lot_id,
start_bid_time,
web_bid,
current_price,
reserve_price,
name,
}) {
super(BID_TYPE.PRODUCT_TAB, url);
this.max_price = max_price || 0;
this.model = model;
this.plus_price = plus_price || 0;
this.first_bid = first_bid;
this.id = id;
this.created_at = created_at;
this.updated_at = updated_at;
this.quantity = quantity;
this.histories = histories;
this.close_time = close_time;
this.lot_id = lot_id;
this.start_bid_time = start_bid_time;
this.web_bid = web_bid;
this.current_price = current_price;
this.name = name;
this.reserve_price = reserve_price;
constructor({
url,
max_price,
plus_price,
model,
first_bid = false,
id,
created_at,
updated_at,
quantity = 1,
histories = [],
close_time,
lot_id,
start_bid_time,
web_bid,
current_price,
reserve_price,
name,
}) {
super(BID_TYPE.PRODUCT_TAB, url);
this.max_price = max_price || 0;
this.model = model;
this.plus_price = plus_price || 0;
this.first_bid = first_bid;
this.id = id;
this.created_at = created_at;
this.updated_at = updated_at;
this.quantity = quantity;
this.histories = histories;
this.close_time = close_time;
this.lot_id = lot_id;
this.start_bid_time = start_bid_time;
this.web_bid = web_bid;
this.current_price = current_price;
this.name = name;
this.reserve_price = reserve_price;
}
setNewData({
url,
max_price,
plus_price,
model,
first_bid = false,
id,
created_at,
updated_at,
quantity = 1,
histories = [],
close_time,
lot_id,
start_bid_time,
web_bid,
current_price,
reserve_price,
name,
}) {
this.max_price = max_price || 0;
this.model = model;
this.plus_price = plus_price || 0;
this.first_bid = first_bid;
this.id = id;
this.created_at = created_at;
this.updated_at = updated_at;
this.quantity = quantity;
this.histories = histories;
this.close_time = close_time;
this.lot_id = lot_id;
this.start_bid_time = start_bid_time;
this.web_bid = web_bid;
this.url = url;
this.current_price = current_price;
this.name = name;
this.reserve_price = reserve_price;
}
puppeteer_connect = async () => {
if (!this.parent_browser_context) {
console.log(
`❌ Connect fail. parent_browser_context is null: ${this.id}`
);
return;
}
setNewData({
url,
max_price,
plus_price,
model,
first_bid = false,
id,
created_at,
updated_at,
quantity = 1,
histories = [],
close_time,
lot_id,
start_bid_time,
web_bid,
current_price,
reserve_price,
name,
}) {
this.max_price = max_price || 0;
this.model = model;
this.plus_price = plus_price || 0;
this.first_bid = first_bid;
this.id = id;
this.created_at = created_at;
this.updated_at = updated_at;
this.quantity = quantity;
this.histories = histories;
this.close_time = close_time;
this.lot_id = lot_id;
this.start_bid_time = start_bid_time;
this.web_bid = web_bid;
this.url = url;
this.current_price = current_price;
this.name = name;
this.reserve_price = reserve_price;
const context = await browser.createBrowserContext();
const statusInit = await this.restoreContext(context);
if (!statusInit) {
console.log(`⚠️ Restore failed.`);
return;
}
puppeteer_connect = async () => {
if (!this.parent_browser_context) {
console.log(`❌ Connect fail. parent_browser_context is null: ${this.id}`);
return;
}
const page = await context.newPage();
const context = await browser.createBrowserContext();
this.page_context = page;
};
const statusInit = await this.restoreContext(context);
async restoreContext(context) {
const filePath = getPathProfile(this.web_bid.origin_url);
if (!statusInit) {
console.log(`⚠️ Restore failed.`);
return;
}
if (!fs.existsSync(filePath)) return false;
const page = await context.newPage();
const contextData = JSON.parse(fs.readFileSync(filePath, "utf8"));
this.page_context = page;
};
// Restore Cookies
await context.setCookie(...contextData.cookies);
async restoreContext(context) {
const filePath = getPathProfile(this.web_bid.origin_url);
return true;
}
if (!fs.existsSync(filePath)) return false;
async gotoLink() {
const page = this.page_context;
const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
// Restore Cookies
await context.setCookie(...contextData.cookies);
return true;
if (page.isClosed()) {
console.error("❌ Page has been closed, cannot navigate.");
return;
}
async gotoLink() {
const page = this.page_context;
console.log("🔄 Starting the bidding process...");
if (page.isClosed()) {
console.error('❌ Page has been closed, cannot navigate.');
return;
}
try {
await page.goto(this.url, { waitUntil: "networkidle2" });
console.log(`✅ Navigated to: ${this.url}`);
console.log('🔄 Starting the bidding process...');
try {
await page.goto(this.url, { waitUntil: 'networkidle2' });
console.log(`✅ Navigated to: ${this.url}`);
await page.bringToFront();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
console.log('👀 Brought the tab to the foreground.');
} catch (error) {
console.error('❌ Error during navigation:', error);
}
await page.bringToFront();
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
);
console.log("👀 Brought the tab to the foreground.");
} catch (error) {
console.error("❌ Error during navigation:", error);
}
}
}

View File

@ -1,70 +1,82 @@
import * as fs from 'fs';
import path from 'path';
import { GrayApiBid } from '../models/grays.com/grays-api-bid.js';
import { GraysProductBid } from '../models/grays.com/grays-product-bid.js';
import { LangtonsApiBid } from '../models/langtons.com.au/langtons-api-bid.js';
import { LangtonsProductBid } from '../models/langtons.com.au/langtons-product-bid.js';
import configs from '../system/config.js';
import CONSTANTS from '../system/constants.js';
import { sanitizeFileName } from '../system/utils.js';
import { LawsonsApiBid } from '../models/lawsons.com.au/lawsons-api-bid.js';
import { LawsonsProductBid } from '../models/lawsons.com.au/lawsons-product-bid.js';
import * as fs from "fs";
import path from "path";
import { GrayApiBid } from "../models/grays.com/grays-api-bid.js";
import { GraysProductBid } from "../models/grays.com/grays-product-bid.js";
import { LangtonsApiBid } from "../models/langtons.com.au/langtons-api-bid.js";
import { LangtonsProductBid } from "../models/langtons.com.au/langtons-product-bid.js";
import { LawsonsApiBid } from "../models/lawsons.com.au/lawsons-api-bid.js";
import { LawsonsProductBid } from "../models/lawsons.com.au/lawsons-product-bid.js";
import { PicklesApiBid } from "../models/pickles.com.au/pickles-api-bid.js";
import { PicklesProductBid } from "../models/pickles.com.au/pickles-product-bid.js";
import configs from "../system/config.js";
import CONSTANTS from "../system/constants.js";
import { sanitizeFileName } from "../system/utils.js";
// Time to update
const TIME = 30 * 1000;
export const handleCloseRemoveProduct = (data) => {
if (!Array.isArray(data)) return;
if (!Array.isArray(data)) return;
data.forEach(async (item) => {
if (item.page_context) {
safeClosePage(item);
}
});
data.forEach(async (item) => {
if (item.page_context) {
safeClosePage(item);
}
});
};
export const createBidProduct = (web, data) => {
switch (web.origin_url) {
case configs.WEB_URLS.GRAYS: {
return new GraysProductBid({ ...data });
}
case configs.WEB_URLS.LANGTONS: {
return new LangtonsProductBid({ ...data });
}
case configs.WEB_URLS.LAWSONS: {
return new LawsonsProductBid({ ...data });
}
switch (web.origin_url) {
case configs.WEB_URLS.GRAYS: {
return new GraysProductBid({ ...data });
}
case configs.WEB_URLS.LANGTONS: {
return new LangtonsProductBid({ ...data });
}
case configs.WEB_URLS.LAWSONS: {
return new LawsonsProductBid({ ...data });
}
case configs.WEB_URLS.PICKLES: {
return new PicklesProductBid({ ...data });
}
}
};
export const createApiBid = (web) => {
switch (web.origin_url) {
case configs.WEB_URLS.GRAYS: {
return new GrayApiBid({ ...web });
}
case configs.WEB_URLS.LANGTONS: {
return new LangtonsApiBid({ ...web });
}
case configs.WEB_URLS.LAWSONS: {
return new LawsonsApiBid({ ...web });
}
switch (web.origin_url) {
case configs.WEB_URLS.GRAYS: {
return new GrayApiBid({ ...web });
}
case configs.WEB_URLS.LANGTONS: {
return new LangtonsApiBid({ ...web });
}
case configs.WEB_URLS.LAWSONS: {
return new LawsonsApiBid({ ...web });
}
case configs.WEB_URLS.PICKLES: {
return new PicklesApiBid({ ...web });
}
}
};
export const deleteProfile = (data) => {
if (!data?.origin_url) return false;
const filePath = path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(data?.origin_url) + '.json');
if (!data?.origin_url) return false;
const filePath = path.join(
CONSTANTS.PROFILE_PATH,
sanitizeFileName(data?.origin_url) + ".json"
);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
return false;
};
export const shouldUpdateProductTab = (productTab) => {
const updatedAt = new Date(productTab.updated_at).getTime();
const now = Date.now();
const updatedAt = new Date(productTab.updated_at).getTime();
const now = Date.now();
return now - updatedAt >= TIME;
return now - updatedAt >= TIME;
};

View File

@ -1,125 +1,143 @@
import axios from '../axios.js';
import fs from 'fs';
import axios from "../axios.js";
import fs from "fs";
export const getBids = async () => {
try {
const { data } = await axios({
method: 'GET',
url: 'bids',
});
try {
const { data } = await axios({
method: "GET",
url: "bids",
});
if (!data || !data?.data) {
console.log('❌ DATA IS NOT FOUND ON SERVER');
return [];
}
return data.data;
} catch (error) {
console.log('❌ ERROR IN SERVER (GET BIDS): ', error);
return [];
if (!data || !data?.data) {
console.log("❌ DATA IS NOT FOUND ON SERVER");
return [];
}
return data.data;
} catch (error) {
console.log("❌ ERROR IN SERVER (GET BIDS): ", error);
return [];
}
};
export const updateBid = async (id, values) => {
try {
const { data } = await axios({
method: 'PUT',
url: 'bids/' + id,
data: values,
});
try {
const { data } = await axios({
method: "PUT",
url: "bids/" + id,
data: values,
});
if (!data || !data?.data) {
console.log('❌ UPDATE FAILURE (UPDATE BID)');
return null;
}
return data.data;
} catch (error) {
console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response);
return null;
if (!data || !data?.data) {
console.log("❌ UPDATE FAILURE (UPDATE BID)");
return null;
}
return data.data;
} catch (error) {
console.log("❌ ERROR IN SERVER: (UPDATE BID) ", error.response);
return null;
}
};
export const outBid = async (id) => {
try {
const { data } = await axios({
method: 'POST',
url: 'bids/out-bid/' + id,
});
try {
const { data } = await axios({
method: "POST",
url: "bids/out-bid/" + id,
});
if (!data || !data?.data) {
console.log('❌ OUT BID UPDATE FAILURE');
return false;
}
return data.data;
} catch (error) {
console.log('❌ ERROR IN SERVER (OUT BID UPDATE): ', error);
return false;
if (!data || !data?.data) {
console.log("❌ OUT BID UPDATE FAILURE");
return false;
}
return data.data;
} catch (error) {
console.log("❌ ERROR IN SERVER (OUT BID UPDATE): ", error);
return false;
}
};
export const pushPrice = async (values) => {
try {
const { data } = await axios({
method: 'POST',
url: 'bid-histories',
data: values,
});
try {
const { data } = await axios({
method: "POST",
url: "bid-histories",
data: values,
});
if (!data || !data?.data) {
console.log('❌ PUSH PRICE FAILURE');
return { status: false, data: [] };
}
return { status: true, data: data.data };
} catch (error) {
console.log('❌ ERROR IN SERVER (PUSH PRICE): ', error?.response);
return { status: false, data: [] };
if (!data || !data?.data) {
console.log("❌ PUSH PRICE FAILURE");
return { status: false, data: [] };
}
return { status: true, data: data.data };
} catch (error) {
console.log("❌ ERROR IN SERVER (PUSH PRICE): ", error?.response);
return { status: false, data: [] };
}
};
export const updateStatusByPrice = async (id, current_price) => {
try {
const { data } = await axios({
method: 'POST',
url: 'bids/update-status/' + id,
data: {
current_price: Number(current_price) | 0,
},
});
try {
const { data } = await axios({
method: "POST",
url: "bids/update-status/" + id,
data: {
current_price: Number(current_price) | 0,
},
});
if (!data || !data?.data) {
console.log('❌ UPDATE STATUS BY PRICE FAILURE');
return { status: false, data: [] };
}
return { status: true, data: data.data };
} catch (error) {
console.log('❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ', {
// response: error.response,
message: error.message,
});
return { status: false, data: [] };
if (!data || !data?.data) {
console.log("❌ UPDATE STATUS BY PRICE FAILURE");
return { status: false, data: [] };
}
return { status: true, data: data.data };
} catch (error) {
console.log("❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ", {
// response: error.response,
message: error.message,
});
return { status: false, data: [] };
}
};
export const updateStatusWork = async (item, filePath) => {
try {
const response = await axios({
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
},
url: `bids/update-status-work/${item.type}/${item.id}`,
data: {
image: fs.createReadStream(filePath),
},
});
fs.unlinkSync(filePath);
try {
const response = await axios({
method: "POST",
headers: {
"Content-Type": "multipart/form-data",
},
url: `bids/update-status-work/${item.type}/${item.id}`,
data: {
image: fs.createReadStream(filePath),
},
});
fs.unlinkSync(filePath);
return response.data?.data;
} catch (error) {
console.error('❌ Upload failed:', error.response?.data || error.message);
return false;
}
return response.data?.data;
} catch (error) {
console.error("❌ Upload failed:", error.response?.data || error.message);
return false;
}
};
export const updateLoginStatus = async (data) => {
try {
const response = await axios({
method: "POST",
url: `bids/update-login-status`,
data,
});
return response.data?.data;
} catch (error) {
console.error(
"❌ UPDATE FAILURE (LOGIN STATUS):",
error.response?.data || error.message
);
return false;
}
};

View File

@ -1,21 +1,21 @@
import axios from '../axios.js';
import axios from "../axios.js";
export const sendMessage = async (values) => {
try {
const { data } = await axios({
method: 'POST',
url: 'notifications/send-messages',
data: values,
});
try {
const { data } = await axios({
method: "POST",
url: "notifications/send-messages",
data: values,
});
if (!data || !data?.data) {
console.log('❌ UPDATE FAILURE (UPDATE Noti)');
return null;
}
return data.data;
} catch (error) {
console.log('❌ ERROR IN SERVER: (UPDATE Noti) ', error);
return null;
if (!data || !data?.data) {
console.log("❌ UPDATE FAILURE (UPDATE Noti)");
return null;
}
return data.data;
} catch (error) {
console.log("❌ ERROR IN SERVER: (UPDATE Noti) ", error.response);
return null;
}
};

View File

@ -1,34 +1,45 @@
const configs = {
AUTO_TRACKING_DELAY: 5000,
AUTO_TRACKING_CLEANING: 10000,
SOCKET_URL: process.env.SOCKET_URL,
WEB_URLS: {
GRAYS: `https://www.grays.com`,
LANGTONS: `https://www.langtons.com.au`,
LAWSONS: `https://www.lawsons.com.au`,
AUTO_TRACKING_DELAY: 5000,
AUTO_TRACKING_CLEANING: 10000,
SOCKET_URL: process.env.SOCKET_URL,
WEB_URLS: {
GRAYS: `https://www.grays.com`,
LANGTONS: `https://www.langtons.com.au`,
LAWSONS: `https://www.lawsons.com.au`,
PICKLES: `https://www.pickles.com.au`,
},
WEB_CONFIGS: {
GRAYS: {
AUTO_CALL_API_TO_TRACKING: 3000,
API_CALL_TO_TRACKING:
"https://www.grays.com/api/Notifications/GetOutBidLots",
},
WEB_CONFIGS: {
GRAYS: {
AUTO_CALL_API_TO_TRACKING: 3000,
API_CALL_TO_TRACKING: 'https://www.grays.com/api/Notifications/GetOutBidLots',
},
LANGTONS: {
AUTO_CALL_API_TO_TRACKING: 5000,
LOGIN_URL: 'https://www.langtons.com.au/account/login',
API_CALL_TO_TRACKING: 'https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData',
},
LAWSONS: {
LOGIN_URL: 'https://www.lawsons.com.au/login?redirectUrl=/my-account/current-bids',
// API_CALL_TO_TRACKING: 'https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData',
API_DETAIL_INFO: (model) => {
return `https://www.lawsons.com.au/api/auctions/lot/v2/liveInfo/${model}`;
},
API_DETAIL_PRODUCT: (model) => {
return `https://www.lawsons.com.au/api/auctions/lot/${model}`;
},
API_CHECKOUT: 'https://www.lawsons.com.au/app/orderBid',
},
LANGTONS: {
AUTO_CALL_API_TO_TRACKING: 5000,
LOGIN_URL: "https://www.langtons.com.au/account/login",
API_CALL_TO_TRACKING:
"https://www.langtons.com.au/on/demandware.store/Sites-langtons-Site/en_AU/Auction-LotsData",
},
LAWSONS: {
LOGIN_URL:
"https://www.lawsons.com.au/login?redirectUrl=/my-account/current-bids",
API_DETAIL_INFO: (model) => {
return `https://www.lawsons.com.au/api/auctions/lot/v2/liveInfo/${model}`;
},
API_DETAIL_PRODUCT: (model) => {
return `https://www.lawsons.com.au/api/auctions/lot/${model}`;
},
API_CHECKOUT: "https://www.lawsons.com.au/app/orderBid",
},
PICKLES: {
LOGIN_URL: "https://www.pickles.com.au/sign-in",
API_DETAIL_PRODUCT: (model) => {
return `https://www.pickles.com.au/api-website/buyer/ms-web-asset-aggregate/v2/api/assets/${model}/wap-item-details`;
},
API_CHECKOUT:
"https://www.pickles.com.au/delegate/secured/bidding/confirm",
},
},
};
export default configs;