diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..cb763e0 Binary files /dev/null and b/.DS_Store differ diff --git a/auto-bid-admin/package-lock.json b/auto-bid-admin/package-lock.json index 07348d1..a428dbb 100644 --- a/auto-bid-admin/package-lock.json +++ b/auto-bid-admin/package-lock.json @@ -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", diff --git a/auto-bid-admin/package.json b/auto-bid-admin/package.json index 9dc3311..9a4930c 100644 --- a/auto-bid-admin/package.json +++ b/auto-bid-admin/package.json @@ -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" }, diff --git a/auto-bid-admin/src/apis/admin.ts b/auto-bid-admin/src/apis/admin.ts index d438bea..4299caa 100644 --- a/auto-bid-admin/src/apis/admin.ts +++ b/auto-bid-admin/src/apis/admin.ts @@ -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) => { }; export const createAdmin = async (admin: Omit) => { - const newData = removeFalsyValues(admin); + const {permissions , ...newData} = removeFalsyValues(admin); + try { const { data } = await axios({ diff --git a/auto-bid-admin/src/apis/bid.ts b/auto-bid-admin/src/apis/bid.ts index f367e72..0813b94 100644 --- a/auto-bid-admin/src/apis/bid.ts +++ b/auto-bid-admin/src/apis/bid.ts @@ -115,6 +115,6 @@ export const getImagesWorking = async (values: (IBid | IWebBid) & { type: string return data; } catch (error) { - handleError(error); + console.log('%csrc/apis/bid.ts:118 error', 'color: #007acc;', error); } }; diff --git a/auto-bid-admin/src/apis/dashboard.ts b/auto-bid-admin/src/apis/dashboard.ts index 98fab82..47d8d7a 100644 --- a/auto-bid-admin/src/apis/dashboard.ts +++ b/auto-bid-admin/src/apis/dashboard.ts @@ -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); + } +}; \ No newline at end of file diff --git a/auto-bid-admin/src/apis/index.ts b/auto-bid-admin/src/apis/index.ts index 41c44ce..b8b10e0 100644 --- a/auto-bid-admin/src/apis/index.ts +++ b/auto-bid-admin/src/apis/index.ts @@ -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; - const data = response.data; + const data = response?.data; if (response.status === HttpStatusCode.Forbidden) return; diff --git a/auto-bid-admin/src/apis/web-bid.ts b/auto-bid-admin/src/apis/web-bid.ts index 175c83c..8faac2e 100644 --- a/auto-bid-admin/src/apis/web-bid.ts +++ b/auto-bid-admin/src/apis/web-bid.ts @@ -1,92 +1,110 @@ -import { generateNestParams, handleError, handleSuccess } from '.'; -import axios from '../lib/axios'; -import { IWebBid } from '../system/type'; -import { removeFalsyValues } from '../utils'; +import { generateNestParams, handleError, handleSuccess } from "."; +import axios from "../lib/axios"; +import { IWebBid } from "../system/type"; +import { removeFalsyValues } from "../utils"; -const BASE_URL = 'web-bids'; +const BASE_URL = "web-bids"; export const getWebBids = async (params: Record) => { - return await axios({ - url: BASE_URL, - params: generateNestParams(params), - withCredentials: true, - method: 'GET', - }); + return await axios({ + url: BASE_URL, + params: generateNestParams(params), + withCredentials: true, + method: "GET", + }); }; -export const createWebBid = async (bid: Omit) => { - const newData = removeFalsyValues(bid); +export const createWebBid = async ( + bid: Omit +) => { + const newData = removeFalsyValues(bid); - try { - const { data } = await axios({ - url: BASE_URL, - withCredentials: true, - method: 'POST', - data: newData, - }); + try { + const { data } = await axios({ + url: BASE_URL, + withCredentials: true, + method: "POST", + data: newData, + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; export const updateWebBid = async (bid: Partial) => { - const { url, password, username, origin_url, active } = removeFalsyValues(bid, ['active']); + const { + url, + password, + username, + origin_url, + active, + arrival_offset_seconds, + // early_login_seconds + } = removeFalsyValues(bid, ["active"]); - try { - const { data } = await axios({ - url: `${BASE_URL}/` + bid.id, - withCredentials: true, - method: 'PUT', - data: { url, password, username, origin_url, active }, - }); + try { + const { data } = await axios({ + url: `${BASE_URL}/` + bid.id, + withCredentials: true, + method: "PUT", + data: { + url, + password, + username, + origin_url, + active, + arrival_offset_seconds, + // early_login_seconds + }, + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; export const deleteWebBid = async (web: IWebBid) => { - try { - const { data } = await axios({ - url: `${BASE_URL}/` + web.id, - withCredentials: true, - method: 'DELETE', - }); + try { + const { data } = await axios({ + url: `${BASE_URL}/` + web.id, + withCredentials: true, + method: "DELETE", + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; export const deletesWebBid = async (web: IWebBid[]) => { - const ids = web.reduce((prev, cur) => { - prev.push(cur.id); - return prev; - }, [] as number[]); - try { - const { data } = await axios({ - url: `${BASE_URL}/deletes`, - withCredentials: true, - method: 'POST', - data: { - ids, - }, - }); + const ids = web.reduce((prev, cur) => { + prev.push(cur.id); + return prev; + }, [] as number[]); + try { + const { data } = await axios({ + url: `${BASE_URL}/deletes`, + withCredentials: true, + method: "POST", + data: { + ids, + }, + }); - handleSuccess(data); + handleSuccess(data); - return data; - } catch (error) { - handleError(error); - } + return data; + } catch (error) { + handleError(error); + } }; diff --git a/auto-bid-admin/src/components/admin/admin-modal.tsx b/auto-bid-admin/src/components/admin/admin-modal.tsx index c65a52f..341b106 100644 --- a/auto-bid-admin/src/components/admin/admin-modal.tsx +++ b/auto-bid-admin/src/components/admin/admin-modal.tsx @@ -127,14 +127,14 @@ export default function AdminModal({ data, onUpdated, ...props }: IAdminModelPro centered >
- - - + + + {!data && ( <> - - + + )} diff --git a/auto-bid-admin/src/components/admin/grant-new-password-modal.tsx b/auto-bid-admin/src/components/admin/grant-new-password-modal.tsx index 20cd805..1940e82 100644 --- a/auto-bid-admin/src/components/admin/grant-new-password-modal.tsx +++ b/auto-bid-admin/src/components/admin/grant-new-password-modal.tsx @@ -87,8 +87,8 @@ export default function GrantNewPasswordModal({ data, onUpdated, ...props }: IAd centered > - - + + - - - - + + + {isIBid(data) ? data.name : "Tracking page"} + + {isIBid(data) && ( + {`Max price: $${data.max_price}`} + )} + {isIBid(data) && ( + {`Current price: $${data.current_price}`} + )} + + {moment(lastUpdate).format("HH:mm:ss DD/MM/YYYY")} + + + + + + - - - ); + + + {statusLabel()} + + + + {isIBid(data) ? data.web_bid.origin_url : data.origin_url} + + + + + + + ); } diff --git a/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx b/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx index a9eee58..42190b1 100644 --- a/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx +++ b/auto-bid-admin/src/components/web-bid/web-bid-modal.tsx @@ -1,118 +1,196 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Button, LoadingOverlay, Modal, ModalProps, TextInput } from '@mantine/core'; -import { useForm, zodResolver } from '@mantine/form'; -import _ from 'lodash'; -import { useEffect, useRef, useState } from 'react'; -import { z } from 'zod'; -import { createWebBid, updateWebBid } from '../../apis/web-bid'; -import { useConfirmStore } from '../../lib/zustand/use-confirm'; -import { IWebBid } from '../../system/type'; -import { extractDomain } from '../../utils'; +import { + Button, + LoadingOverlay, + Modal, + ModalProps, + NumberInput, + TextInput, +} from "@mantine/core"; +import { useForm, zodResolver } from "@mantine/form"; +import _ from "lodash"; +import { useEffect, useRef, useState } from "react"; +import { z } from "zod"; +import { createWebBid, updateWebBid } from "../../apis/web-bid"; +import { useConfirmStore } from "../../lib/zustand/use-confirm"; +import { IWebBid } from "../../system/type"; +import { extractDomain } from "../../utils"; export interface IWebBidModelProps extends ModalProps { - data: IWebBid | null; - onUpdated?: () => void; + data: IWebBid | null; + onUpdated?: () => void; } const schema = { - url: z.string({ message: 'Url is required' }).url('Invalid url format'), + url: z.string({ message: "Url is required" }).url("Invalid url format"), + arrival_offset_seconds: z + .number({ message: "Arrival offset seconds is required" }) + .refine((val) => val >= 60, { + message: "Arrival offset seconds must be at least 60 seconds (1 minute)", + }), + early_login_seconds: z + .number({ message: "Early login seconds is required" }) + .refine((val) => val >= 600, { + message: "Early login seconds must be at least 600 seconds (10 minute)", + }), }; -export default function WebBidModal({ data, onUpdated, ...props }: IWebBidModelProps) { - const form = useForm({ - validate: zodResolver(z.object(schema)), - }); +export default function WebBidModal({ + data, + onUpdated, + ...props +}: IWebBidModelProps) { + const form = useForm({ + validate: zodResolver(z.object(schema)), + }); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); - const prevData = useRef(data); + const prevData = useRef(data); - const { setConfirm } = useConfirmStore(); + const { setConfirm } = useConfirmStore(); - const handleSubmit = async (values: typeof form.values) => { - if (data) { - setConfirm({ - title: 'Update ?', - message: `This web will be update`, - handleOk: async () => { - setLoading(true); - const result = await updateWebBid(values); - setLoading(false); + const handleSubmit = async (values: typeof form.values) => { + if (data) { + setConfirm({ + title: "Update ?", + message: `This web will be update`, + handleOk: async () => { + setLoading(true); + console.log( + "%csrc/components/web-bid/web-bid-modal.tsx:54 values", + "color: #007acc;", + values + ); + const result = await updateWebBid(values); + setLoading(false); - if (!result) return; + if (!result) return; - props.onClose(); + props.onClose(); - if (onUpdated) { - onUpdated(); - } - }, - okButton: { - color: 'blue', - value: 'Update', - }, - }); - } else { - const { url, origin_url } = values; + if (onUpdated) { + onUpdated(); + } + }, + okButton: { + color: "blue", + value: "Update", + }, + }); + } else { + const { url, origin_url, arrival_offset_seconds, early_login_seconds } = values; - setLoading(true); - const result = await createWebBid({ url, origin_url } as IWebBid); - setLoading(false); + setLoading(true); + const result = await createWebBid({ + url, + origin_url, + arrival_offset_seconds, + early_login_seconds + } as IWebBid); + setLoading(false); - if (!result) return; + if (!result) return; - props.onClose(); + props.onClose(); - if (onUpdated) { - onUpdated(); - } - } - }; + if (onUpdated) { + onUpdated(); + } + } + }; - useEffect(() => { - form.reset(); - if (!data) return; + useEffect(() => { + form.reset(); + if (!data) return; - form.setValues(data); + form.setValues(data); - prevData.current = data; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data]); + prevData.current = data; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); - useEffect(() => { - if (!props.opened) { - form.reset(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.opened]); + useEffect(() => { + if (!props.opened) { + form.reset(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.opened]); - useEffect(() => { - if (form.values?.url) { - form.setFieldValue('origin_url', extractDomain(form.values.url)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form.values]); + useEffect(() => { + if (form.values?.url) { + form.setFieldValue("origin_url", extractDomain(form.values.url)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [form.values]); - return ( - Web} - centered + return ( + Web} + centered + > + + + + + + {/* */} + + + - - - - - - ); + + + ); } diff --git a/auto-bid-admin/src/constant/index.ts b/auto-bid-admin/src/constant/index.ts new file mode 100644 index 0000000..9daa38e --- /dev/null +++ b/auto-bid-admin/src/constant/index.ts @@ -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 \ No newline at end of file diff --git a/auto-bid-admin/src/lib/zustand/use-status-tool-store.ts b/auto-bid-admin/src/lib/zustand/use-status-tool-store.ts new file mode 100644 index 0000000..703e94d --- /dev/null +++ b/auto-bid-admin/src/lib/zustand/use-status-tool-store.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +type TStatusToolState = { + statusTool: string | boolean; + setStatusTool: (value: TStatusToolState["statusTool"]) => void; +}; + +export const useStatusToolStore = create((set) => ({ + statusTool: false, + props: {}, + setStatusTool: (value: TStatusToolState["statusTool"]) => + set({ statusTool: value }), +})); diff --git a/auto-bid-admin/src/pages/bids.tsx b/auto-bid-admin/src/pages/bids.tsx index 0f1d06f..8edd0d2 100644 --- a/auto-bid-admin/src/pages/bids.tsx +++ b/auto-bid-admin/src/pages/bids.tsx @@ -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 = useRef({}); + const refTableFn: TRefTableFn = useRef({}); - const [clickData, setClickData] = useState(null); + const [clickData, setClickData] = useState(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[] = [ - { - key: 'id', - title: 'ID', - typeFilter: 'number', - }, - { - key: 'name', - title: 'Name', - typeFilter: 'text', - }, - { - key: 'web_bid', - title: 'Web', - typeFilter: 'text', - renderRow(row) { - return {row.web_bid.origin_url}; - }, - }, - { - 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[] = [ + { + key: "id", + title: "ID", + typeFilter: "number", + }, + { + key: "name", + title: "Name", + typeFilter: "text", + }, + { + key: "web_bid", + title: "Web", + typeFilter: "text", + renderRow(row) { + return {row.web_bid.origin_url}; + }, + }, + { + key: "lot_id", + title: "Lot ID", + typeFilter: "text", + }, + { + key: "model", + title: "Model", + typeFilter: "text", + }, - return {bidPrice ? bidPrice.price : 'None'}; - }, - }, - { - key: 'start_bid_time', - title: 'Start bid', - typeFilter: 'text', - renderRow(row) { - return ( - - ); - }, - }, - { - key: 'close_time', - title: 'Close time', - typeFilter: 'text', - renderRow(row) { - return ( - - ); - }, - }, - { - key: 'status', - title: 'Status', - typeFilter: 'text', - renderRow(row) { - return ( - - - {row.status} - - - ); - }, - }, - ]; + { + 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 {bidPrice ? bidPrice.price : "None"}; + }, + }, + { + key: "start_bid_time", + title: "Start bid", + typeFilter: "text", + renderRow(row) { return ( - { - 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: Action, - body: (row) => { - return ( - - - e.stopPropagation()} className="flex w-full items-center justify-center"> - - - - - - - e.stopPropagation()}> - { - setClickData(row); - bidModal.open(); - }} - leftSection={} - > - Edit - - - { - setClickData(row); - historiesModel.open(); - }} - leftSection={} - > - Histories - - {['https://www.grays.com'].includes(row?.web_bid.origin_url) && ( - { - setClickData(row); - historiesGraysApiModel.open(); - }} - leftSection={} - > - Bids - - )} - - handleToggleBid(row)} - leftSection={row.status === 'biding' ? : } - > - {row.status === 'biding' ? 'Disable' : 'Enable'} - - - handleDelete(row)} leftSection={}> - Delete - - - - ); - }, - }} - rowKey="id" - /> + ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, + }, + { + key: "close_time", + title: "Close time", + typeFilter: "text", + renderRow(row) { + return ( + + ); + }, + }, + { + key: "status", + title: "Status", + typeFilter: "text", + renderRow(row) { + return ( + + + {row.status} + + + ); + }, + }, + ]; + 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 ( - - {table} +
{ + 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); - { - historiesModel.close(); - setClickData(null); - }} - data={clickData} - /> - { - 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: Action, + body: (row) => { + return ( + + + e.stopPropagation()} + className="flex w-full items-center justify-center" + > + + + + + - setClickData(null); - }} - opened={openedBid} - onClose={() => { - bidModal.close(); + e.stopPropagation()}> + { + setClickData(row); + bidModal.open(); + }} + leftSection={} + > + Edit + - setClickData(null); - }} - data={clickData} - /> + { + setClickData(row); + historiesModel.open(); + }} + leftSection={} + > + Histories + + {haveHistories.includes(row?.web_bid.origin_url) && ( + { + setClickData(row); + switch (row.web_bid.origin_url) { + case constants.grays: { + historiesGraysApiModel.open(); + break; + } + case constants.pickles: { + historiesPicklesApiModel.open(); + break; + } + default: { + historiesGraysApiModel.open(); + } + } + }} + leftSection={} + > + Bids + + )} - { - if (refTableFn.current?.fetchData) { - refTableFn.current.fetchData(); + handleToggleBid(row)} + leftSection={ + row.status === "biding" ? ( + + ) : ( + + ) } + > + {row.status === "biding" ? "Disable" : "Enable"} + - setClickData(null); - }} - opened={openedHistoriesGraysApi} - onClose={() => { - historiesGraysApiModel.close(); - - setClickData(null); - }} - data={clickData} - /> - + handleDelete(row)} + leftSection={} + > + Delete + + + + ); + }, + }} + rowKey="id" + /> ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {table} + { + historiesModel.close(); + setClickData(null); + }} + data={clickData} + /> + { + if (refTableFn.current?.fetchData) { + refTableFn.current.fetchData(); + } + + setClickData(null); + }} + opened={openedBid} + onClose={() => { + bidModal.close(); + + setClickData(null); + }} + data={clickData} + /> + {/* Grays */} + {openedHistoriesGraysApi && ( + { + if (refTableFn.current?.fetchData) { + refTableFn.current.fetchData(); + } + + setClickData(null); + }} + opened={openedHistoriesGraysApi} + onClose={() => { + historiesGraysApiModel.close(); + + setClickData(null); + }} + data={clickData} + /> + )} + + {openedHistoriesPicklesApi && ( + { + if (refTableFn.current?.fetchData) { + refTableFn.current.fetchData(); + } + + setClickData(null); + }} + opened={true} + onClose={() => { + historiesPicklesApiModel.close(); + setClickData(null); + }} + data={clickData} + /> + )} + + ); } diff --git a/auto-bid-admin/src/pages/dashboard.tsx b/auto-bid-admin/src/pages/dashboard.tsx index db8961e..b8e0218 100644 --- a/auto-bid-admin/src/pages/dashboard.tsx +++ b/auto-bid-admin/src/pages/dashboard.tsx @@ -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 ( - - - - Admin Dashboard - - - - - - - - {workingData.length > 0 && workingData.map((item, 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 && ( - - No Pages - - )} - + 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" }, + }); + }; - - - ); + return ( + + + + Admin Dashboard + + + + + + + + + + + {workingData.length > 0 && + workingData.map((item, index) => ( + + ))} + + {workingData.length <= 0 && ( + + No Pages + + )} + + + + + ); } diff --git a/auto-bid-admin/src/system/type/index.ts b/auto-bid-admin/src/system/type/index.ts index b8a7b85..28f68ed 100644 --- a/auto-bid-admin/src/system/type/index.ts +++ b/auto-bid-admin/src/system/type/index.ts @@ -18,24 +18,7 @@ export interface ITimestamp { updated_at: string; } -export interface IBid extends ITimestamp { - id: number; - max_price: number; - reserve_price: number; - current_price: number; - name: string | null; - quantity: number; - url: string; - model: string; - lot_id: string; - plus_price: number; - close_time: string | null; - start_bid_time: string | null; - first_bid: boolean; - status: 'biding' | 'out-bid' | 'win-bid'; - histories: IHistory[]; - web_bid: IWebBid; -} + export interface IHistory extends ITimestamp { id: number; @@ -59,9 +42,30 @@ export interface IWebBid extends ITimestamp { username: string | null; password: string | null; active: boolean; + arrival_offset_seconds: number; + early_login_seconds: number; children: IBid[]; } +export interface IBid extends ITimestamp { + id: number; + max_price: number; + reserve_price: number; + current_price: number; + name: string | null; + quantity: number; + url: string; + model: string; + lot_id: string; + plus_price: number; + close_time: string | null; + start_bid_time: string | null; + first_bid: boolean; + status: 'biding' | 'out-bid' | 'win-bid'; + histories: IHistory[]; + web_bid: IWebBid; +} + export interface IPermission extends ITimestamp { id: number; name: string; diff --git a/auto-bid-admin/src/utils/index.ts b/auto-bid-admin/src/utils/index.ts index f0f9716..02957ea 100644 --- a/auto-bid-admin/src/utils/index.ts +++ b/auto-bid-admin/src/utils/index.ts @@ -1,112 +1,178 @@ /* 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"; +import { IWebBid } from "../system/type"; 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>(obj: T, excludeKeys: (keyof T)[] = []): Partial { - 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); +export function removeFalsyValues>( + obj: T, + excludeKeys: (keyof T)[] = [] +): Partial { + 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); } 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]; +} + +export function findEarlyLoginTime(webBid: IWebBid): string | null { + const now = new Date(); + + // Bước 1: Lọc ra những bid có close_time hợp lệ + const validChildren = webBid.children.filter(child => child.close_time); + + if (validChildren.length === 0) return null; + + // Bước 2: Tìm bid có close_time gần hiện tại nhất + const closestBid = validChildren.reduce((closest, current) => { + const closestDiff = Math.abs(new Date(closest.close_time!).getTime() - now.getTime()); + const currentDiff = Math.abs(new Date(current.close_time!).getTime() - now.getTime()); + return currentDiff < closestDiff ? current : closest; + }); + + if (!closestBid.close_time) return null; + + // Bước 3: Tính toán thời gian login sớm + const closeTime = new Date(closestBid.close_time); + closeTime.setSeconds(closeTime.getSeconds() - (webBid.early_login_seconds || 0)); + + return closeTime.toISOString(); } diff --git a/auto-bid-server/bot-data/metadata.json b/auto-bid-server/bot-data/metadata.json index 3283895..93d37ea 100644 --- a/auto-bid-server/bot-data/metadata.json +++ b/auto-bid-server/bot-data/metadata.json @@ -1 +1,5 @@ -{"createdAt":1744861741554} \ No newline at end of file +<<<<<<< HEAD +{"createdAt":1745827424853} +======= +{"createdAt":1746413672600} +>>>>>>> 26b10a7 (pickxel and fix login) diff --git a/auto-bid-server/src/modules/bids/apis/grays.api.ts b/auto-bid-server/src/modules/bids/apis/grays.api.ts index 4b27e38..23da68d 100644 --- a/auto-bid-server/src/modules/bids/apis/grays.api.ts +++ b/auto-bid-server/src/modules/bids/apis/grays.api.ts @@ -2,18 +2,56 @@ 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}¤cyCode=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){ + + // GRAYS + case 'https://www.grays.com': { + const response = await axios({ + url: `https://www.grays.com/api/LotInfo/GetBiddingHistory?lotId=${lot_id}¤cyCode=AUD`, + }); + + if (response.data && response.data?.Bids) { + return AppResponse.toResponse(response.data.Bids); + } + + return AppResponse.toResponse([]) + } + + // PICKLES + 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.Bids); + } + + return AppResponse.toResponse([]) + } + default: + return AppResponse.toResponse([]) } + + } catch (error) { return AppResponse.toResponse([]); } diff --git a/auto-bid-server/src/modules/bids/controllers/admin/admin-dashboard.controller.ts b/auto-bid-server/src/modules/bids/controllers/admin/admin-dashboard.controller.ts index 9816269..dd8d2d3 100644 --- a/auto-bid-server/src/modules/bids/controllers/admin/admin-dashboard.controller.ts +++ b/auto-bid-server/src/modules/bids/controllers/admin/admin-dashboard.controller.ts @@ -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(); + } } diff --git a/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts b/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts index 5894a44..1b2b433 100644 --- a/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts +++ b/auto-bid-server/src/modules/bids/controllers/client/bids.controller.ts @@ -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,11 +69,18 @@ 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), { diff --git a/auto-bid-server/src/modules/bids/dto/bid/client-update-login-status.dto.ts b/auto-bid-server/src/modules/bids/dto/bid/client-update-login-status.dto.ts new file mode 100644 index 0000000..cceb978 --- /dev/null +++ b/auto-bid-server/src/modules/bids/dto/bid/client-update-login-status.dto.ts @@ -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 +} diff --git a/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts b/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts index a0bcff4..9acee56 100644 --- a/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts +++ b/auto-bid-server/src/modules/bids/dto/web-bid/update-web-bid.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsOptional, IsString, IsUrl } from 'class-validator'; +import { IsBoolean, IsNumber, IsOptional, IsString, IsUrl, Min } from 'class-validator'; export class UpdateWebBidDto { @IsUrl() @@ -9,6 +9,16 @@ export class UpdateWebBidDto { @IsOptional() url: string; + @IsNumber() + @Min(60) + @IsOptional() + arrival_offset_seconds: number; + + @IsNumber() + @Min(600) + @IsOptional() + early_login_seconds: number; + @IsString() @IsOptional() username: string; diff --git a/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts b/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts index 5111002..4fce0fd 100644 --- a/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts +++ b/auto-bid-server/src/modules/bids/entities/wed-bid.entity.ts @@ -17,6 +17,12 @@ export class WebBid extends Timestamp { @Column({ default: null, nullable: true }) username: string; + @Column({ default: 300 }) + arrival_offset_seconds: number; + + @Column({ default: 600 }) + early_login_seconds: number; + @Column({ default: null, nullable: true }) @Exclude() password: string; diff --git a/auto-bid-server/src/modules/bids/getways/admin-bid-getway.ts b/auto-bid-server/src/modules/bids/getways/admin-bid-getway.ts index 1dec825..59714b1 100644 --- a/auto-bid-server/src/modules/bids/getways/admin-bid-getway.ts +++ b/auto-bid-server/src/modules/bids/getways/admin-bid-getway.ts @@ -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(); } diff --git a/auto-bid-server/src/modules/bids/services/bids.service.ts b/auto-bid-server/src/modules/bids/services/bids.service.ts index a289f37..c1da5de 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -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 { @@ -227,7 +228,7 @@ export class BidsService { if (!bid.close_time && !bid.start_bid_time) { // Thiết lập thời gian bắt đầu là 5 phút trước khi đóng // bid.start_bid_time = new Date().toUTCString(); - bid.start_bid_time = subtractMinutes(close_time, 5); + bid.start_bid_time = subtractMinutes(close_time, bid.web_bid.arrival_offset_seconds/ 60); } // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc) @@ -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) + } } diff --git a/auto-bid-server/src/modules/bids/services/dashboard.service.ts b/auto-bid-server/src/modules/bids/services/dashboard.service.ts index 7485196..fb11d04 100644 --- a/auto-bid-server/src/modules/bids/services/dashboard.service.ts +++ b/auto-bid-server/src/modules/bids/services/dashboard.service.ts @@ -7,7 +7,7 @@ export class DashboardService { private readonly tool_name = 'auto-bid-tool'; - async resetToolByName(toolName: string): Promise { + async resetProcessByName(toolName: string): Promise { 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 { + async getStatusProcessByName(toolName: string): Promise { + 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 { 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); + } + } } diff --git a/auto-bid-server/src/modules/bids/services/web-bids.service.ts b/auto-bid-server/src/modules/bids/services/web-bids.service.ts index 96e028f..9a48eb1 100644 --- a/auto-bid-server/src/modules/bids/services/web-bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/web-bids.service.ts @@ -80,7 +80,7 @@ export class WebBidsService { async emitAccountUpdate(id: WebBid['id']) { const data = await this.webBidRepo.findOne({ where: { id, children: { status: 'biding' } }, - relations: { children: true }, + relations: { children: { web_bid: true } }, }); this.eventEmitter.emit(Event.WEB_UPDATED, data || null); diff --git a/auto-bid-server/src/modules/bids/utils/events.ts b/auto-bid-server/src/modules/bids/utils/events.ts index 07df96c..f3d7f3b 100644 --- a/auto-bid-server/src/modules/bids/utils/events.ts +++ b/auto-bid-server/src/modules/bids/utils/events.ts @@ -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}`; + } } diff --git a/auto-bid-server/src/modules/databases/databases.module.ts b/auto-bid-server/src/modules/databases/databases.module.ts index 59e1737..a1aea81 100644 --- a/auto-bid-server/src/modules/databases/databases.module.ts +++ b/auto-bid-server/src/modules/databases/databases.module.ts @@ -7,18 +7,21 @@ import { TypeOrmModule } from '@nestjs/typeorm'; TypeOrmModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ - type: 'mysql', - host: configService.get('DB_HOST'), - port: configService.get('DB_PORT'), - username: configService.get('DB_USERNAME'), - password: configService.get('DB_PASSWORD'), - database: configService.get('DB_NAME'), - charset: 'utf8mb4_unicode_ci', - entities: ['dist/**/*.entity{.ts,.js}'], - synchronize: - configService.get('ENVIRONMENT') === 'prod' ? false : true, - }), + useFactory: (configService: ConfigService) => { + + return { + type: 'mysql', + host: configService.get('DB_HOST'), + port: configService.get('DB_PORT'), + username: configService.get('DB_USERNAME'), + password: configService.get('DB_PASSWORD'), + database: configService.get('DB_NAME'), + charset: 'utf8mb4_unicode_ci', + entities: ['dist/**/*.entity{.ts,.js}'], + synchronize: + configService.get('ENVIRONMENT') === 'prod' ? false : true, + } + }, }), ], }) diff --git a/auto-bid-server/src/ultils/index.ts b/auto-bid-server/src/ultils/index.ts index c7d2231..7da0c74 100644 --- a/auto-bid-server/src/ultils/index.ts +++ b/auto-bid-server/src/ultils/index.ts @@ -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; + } } } diff --git a/auto-bid-tool/ecosystem.config.cjs b/auto-bid-tool/ecosystem.config.cjs new file mode 100644 index 0000000..dd06342 --- /dev/null +++ b/auto-bid-tool/ecosystem.config.cjs @@ -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", + }, + ], +}; diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js index 7fd99bf..c64bf3b 100644 --- a/auto-bid-tool/index.js +++ b/auto-bid-tool/index.js @@ -1,11 +1,22 @@ -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, + findEarlyLoginTime, + isTimeReached, + safeClosePage, +} from "./system/utils.js"; +import { updateLoginStatus } from "./system/apis/bid.js"; global.IS_CLEANING = true; @@ -14,260 +25,478 @@ 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 addProductTab = (data) => { + if ( + typeof data !== "object" || + data === null || + !Array.isArray(data.children) + ) { + console.warn("Data must be an object with a children array"); + return; + } + + const { children, ...web } = data; + + if (children.length === 0) { + console.warn( + `⚠️ No children found for bid id ${web.id}, skipping addProductTab` + ); + return; + } + + const managerBidMap = new Map(MANAGER_BIDS.map((bid) => [bid.id, bid])); + const prevApiBid = managerBidMap.get(web.id); + + if (prevApiBid) { + // Cập nhật + const updatedChildren = prevApiBid.children; + + children.forEach((newChild) => { + const existingChildIndex = updatedChildren.findIndex( + (c) => c.id === newChild.id + ); + + if (existingChildIndex !== -1) { + updatedChildren[existingChildIndex].setNewData(newChild); + } else { + updatedChildren.push(createBidProduct(web, newChild)); + } + }); + + prevApiBid.setNewData({ ...web, children: updatedChildren }); + } else { + // Tạo mới + const newChildren = children.map((item) => createBidProduct(web, item)); + const newApiBid = createApiBid({ ...web, children: newChildren }); + + MANAGER_BIDS.push(newApiBid); + + console.log("%cindex.js:116 {MANAGER_BIDS}", "color: #007acc;", { + MANAGER_BIDS, + children: newChildren, + }); + } }; const tracking = async () => { - console.log('🚀 Tracking process started...'); + 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(); + }) + ); + + await Promise.allSettled( + MANAGER_BIDS.filter((bid) => !bid.page_context).map((apiBid) => { + console.log(`🎧 Listening to events for close login: ${apiBid.id}`); + return (apiBid.onCloseLogin = (data) => { + // Loại bỏ class hiện có. Tạo tiền đề cho việc tạo đối tượng mới lại + MANAGER_BIDS = MANAGER_BIDS.filter( + (item) => item.id !== data.id && item.type !== data.type ); - Promise.allSettled( - productTabs.map(async (productTab) => { - console.log(`📌 Processing Product ID: ${productTab.id}`); + addProductTab(data); + }); + }) + ); - // 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; - } - } + Promise.allSettled( + productTabs.map(async (productTab) => { + console.log(`📌 Processing Product ID: ${productTab.id}`); - // 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(); - } + // 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; + } + } - // 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(); - }), + // 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(); + } - // Dọn dẹp tab không dùng - console.log('🧹 Cleaning up unused tabs...'); - clearLazyTab(); + // 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 trạng thái tracking - console.log('📊 Tracking work status...'); - workTracking(); - } catch (error) { - console.error('❌ Error in tracking loop:', error); - } + // 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.` + ); + } - console.log(`⏳ Waiting ${configs.AUTO_TRACKING_DELAY / 1000} seconds before the next iteration...`); - await delay(configs.AUTO_TRACKING_DELAY); + // 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(); + }) + ); + + // 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 (!browser) { +// console.warn("⚠️ Browser is not available or disconnected."); +// return; +// } + +// try { +// const pages = await browser.pages(); + +// // Lấy danh sách URL từ flattenedArray +// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [ +// item.url, +// ...item.children.map((child) => child.url), +// ]).filter(Boolean); // Lọc bỏ null hoặc undefined + +// console.log( +// "🔍 Page URLs:", +// pages.map((page) => page.url()) +// ); + +// for (const page of pages) { +// const pageUrl = page.url(); + +// if (!pageUrl || pageUrl === "about:blank") continue; + +// if (!activeUrls.includes(pageUrl)) { +// if (!page.isClosed() && browser.isConnected()) { +// try { +// const bidData = MANAGER_BIDS.filter((item) => item.page_context) +// .map((i) => ({ +// current_url: i.page_context.url(), +// data: i, +// })) +// .find((j) => j.current_url === pageUrl); + +// console.log(bidData); + +// if (bidData && bidData.data) { +// await safeClosePage(bidData.data); +// } else { +// // 👇 Wrap close with timeout + error catch +// await Promise.race([ +// page.close(), +// new Promise((_, reject) => +// setTimeout(() => reject(new Error("Close timeout")), 3000) +// ), +// ]); +// } + +// console.log(`🛑 Closing unused tab: ${pageUrl}`); +// } catch (err) { +// console.warn(`⚠️ Error closing tab ${pageUrl}: ${err.message}`); +// } +// } +// } +// } +// } catch (err) { +// console.error("❌ Error in clearLazyTab:", err.message); +// } +// }; + const clearLazyTab = async () => { - if (!global.IS_CLEANING) { - console.log('🚀 Cleaning flag is OFF. Proceeding with operation.'); - 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(); + console.log("🔍 Found pages:", pages.length); - // Lấy danh sách URL từ flattenedArray - const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [item.url, ...item.children.map((child) => child.url)]).filter(Boolean); // Lọc bỏ null hoặc undefined + const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [ + item.url, + ...item.children.map((child) => child.url), + ]).filter(Boolean); - console.log( - '🔍 Page URLs:', - pages.map((page) => page.url()), - ); + for (const page of pages) { + try { + if (page.isClosed()) continue; // Trang đã đóng thì bỏ qua - for (const page of pages) { - const pageUrl = page.url(); + const pageUrl = page.url(); - // 🔥 Bỏ qua tab 'about:blank' hoặc tab không có URL - if (!pageUrl || pageUrl === 'about:blank') continue; + if (!pageUrl || pageUrl === "about:blank") continue; + if (activeUrls.includes(pageUrl)) 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); + page.removeAllListeners(); - console.log(bidData); + console.log(`🛑 Unused page detected: ${pageUrl}`); - if (bidData && bidData.data) { - await safeClosePage(bidData.data); - } else { - await page.close(); - } + 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(`🛑 Closing unused tab: ${pageUrl}`); - } catch (err) { - console.warn(`⚠️ Error closing tab ${pageUrl}:, err.message`); - } - } - } + if (bidData && bidData.data) { + await safeClosePage(bidData.data); + } else { + try { + await Promise.race([ + page.close(), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Close timeout")), 3000) + ), + ]); + } catch (closeErr) { + console.warn( + `⚠️ Error closing page ${pageUrl}: ${closeErr.message}` + ); + } } - } catch (err) { - console.error('❌ Error in clearLazyTab:', err.message); + + console.log(`✅ Closed page: ${pageUrl}`); + } catch (pageErr) { + console.warn(`⚠️ Error handling page: ${pageErr.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.'); - } - }); + MANAGER_BIDS = MANAGER_BIDS.filter((item) => item.id != data.id); - // AUTO TRACKING - tracking(); + addProductTab(data); + + global.IS_CLEANING = true; + } else { + console.log("⚠️ No profile found to delete."); + } + }); + + // AUTO TRACKING + tracking(); })(); diff --git a/auto-bid-tool/models/api-bid.js b/auto-bid-tool/models/api-bid.js index 548aa06..7d82433 100644 --- a/auto-bid-tool/models/api-bid.js +++ b/auto-bid-tool/models/api-bid.js @@ -1,106 +1,144 @@ -import * as fs from 'fs'; -import path from 'path'; -import BID_TYPE from '../system/bid-type.js'; -import browser from '../system/browser.js'; -import CONSTANTS from '../system/constants.js'; -import { getPathProfile, sanitizeFileName } from '../system/utils.js'; -import { Bid } from './bid.js'; +import * as fs from "fs"; +import path from "path"; +import BID_TYPE from "../system/bid-type.js"; +import browser from "../system/browser.js"; +import CONSTANTS from "../system/constants.js"; +import { + findEarlyLoginTime, + getPathProfile, + isTimeReached, + sanitizeFileName, +} from "../system/utils.js"; +import { Bid } from "./bid.js"; export class ApiBid extends Bid { - id; - account; - children = []; - children_processing = []; - created_at; - updated_at; - origin_url; - active; - browser_context; - username; - password; + id; + account; + children = []; + children_processing = []; + created_at; + updated_at; + origin_url; + active; + browser_context; + username; + password; - constructor({ url, username, password, id, children, created_at, updated_at, origin_url, active }) { - super(BID_TYPE.API_BID, url); + constructor({ + url, + username, + password, + id, + children, + created_at, + updated_at, + origin_url, + active, + }) { + super(BID_TYPE.API_BID, url); - this.created_at = created_at; - this.updated_at = updated_at; - this.children = children; - this.origin_url = origin_url; - this.active = active; - this.username = username; - this.password = password; - this.id = id; + this.created_at = created_at; + this.updated_at = updated_at; + this.children = children; + this.origin_url = origin_url; + this.active = active; + this.username = username; + this.password = password; + this.id = id; + } + + setNewData({ + url, + username, + password, + id, + children, + created_at, + updated_at, + origin_url, + active, + }) { + this.created_at = created_at; + this.updated_at = updated_at; + this.children = children; + this.origin_url = origin_url; + this.active = active; + this.username = username; + this.password = password; + this.url = url; + } + + puppeteer_connect = async () => { + this.browser_context = await browser.createBrowserContext(); + + const page = await this.browser_context.newPage(); + + this.page_context = page; + + await this.restoreContext(); + }; + + listen_events = async () => { + if (this.page_context) return; + + await this.puppeteer_connect(); + + await this.action(); + + await this.saveContext(); + }; + + async saveContext() { + if (!this.browser_context || !this.page_context) return; + + try { + const cookies = await this.browser_context.cookies(); + const localStorageData = await this.page_context.evaluate(() => + JSON.stringify(localStorage) + ); + + const contextData = { + cookies, + localStorage: localStorageData, + }; + + const dirPath = path.join(CONSTANTS.PROFILE_PATH); + + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + console.log(`📂 Save at folder: ${dirPath}`); + } + + fs.writeFileSync( + path.join(dirPath, sanitizeFileName(this.origin_url) + ".json"), + JSON.stringify(contextData, null, 2) + ); + console.log("✅ Context saved!"); + } catch (error) { + console.log("Save Context: ", error.message); } + } - setNewData({ url, username, password, id, children, created_at, updated_at, origin_url, active }) { - this.created_at = created_at; - this.updated_at = updated_at; - this.children = children; - this.origin_url = origin_url; - this.active = active; - this.username = username; - this.password = password; - this.url = url; - } + async restoreContext() { + if (!this.browser_context || !this.page_context) return; - puppeteer_connect = async () => { - this.browser_context = await browser.createBrowserContext(); + const filePath = getPathProfile(this.origin_url); - const page = await this.browser_context.newPage(); + if (!fs.existsSync(filePath)) return; - this.page_context = page; + const contextData = JSON.parse(fs.readFileSync(filePath, "utf8")); - await this.restoreContext(); - }; + // Restore Cookies + await this.page_context.setCookie(...contextData.cookies); - listen_events = async () => { - if (this.page_context) return; + console.log("🔄 Context restored!"); + } - await this.puppeteer_connect(); + async onCloseLogin() {} - await this.action(); + async isTimeToLogin() { + const earlyLoginTime = findEarlyLoginTime(this); - await this.saveContext(); - }; - - async saveContext() { - if (!this.browser_context || !this.page_context) return; - - try { - const cookies = await this.browser_context.cookies(); - const localStorageData = await this.page_context.evaluate(() => JSON.stringify(localStorage)); - - const contextData = { - cookies, - localStorage: localStorageData, - }; - - const dirPath = path.join(CONSTANTS.PROFILE_PATH); - - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - console.log(`📂 Save at folder: ${dirPath}`); - } - - fs.writeFileSync(path.join(dirPath, sanitizeFileName(this.origin_url) + '.json'), JSON.stringify(contextData, null, 2)); - console.log('✅ Context saved!'); - } catch (error) { - console.log('Save Context: ', error.message); - } - } - - async restoreContext() { - if (!this.browser_context || !this.page_context) return; - - const filePath = getPathProfile(this.origin_url); - - if (!fs.existsSync(filePath)) return; - - const contextData = JSON.parse(fs.readFileSync(filePath, 'utf8')); - - // Restore Cookies - await this.page_context.setCookie(...contextData.cookies); - - console.log('🔄 Context restored!'); - } + return earlyLoginTime && isTimeReached(earlyLoginTime); + } } diff --git a/auto-bid-tool/models/bid.js b/auto-bid-tool/models/bid.js index 4cc560e..bef84ea 100644 --- a/auto-bid-tool/models/bid.js +++ b/auto-bid-tool/models/bid.js @@ -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() {} } diff --git a/auto-bid-tool/models/grays.com/grays-api-bid.js b/auto-bid-tool/models/grays.com/grays-api-bid.js index b6fcce5..bdfbbda 100644 --- a/auto-bid-tool/models/grays.com/grays-api-bid.js +++ b/auto-bid-tool/models/grays.com/grays-api-bid.js @@ -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}`); + } + }; } diff --git a/auto-bid-tool/models/grays.com/grays-product-bid.js b/auto-bid-tool/models/grays.com/grays-product-bid.js index 7a38b64..ea1513a 100644 --- a/auto-bid-tool/models/grays.com/grays-product-bid.js +++ b/auto-bid-tool/models/grays.com/grays-product-bid.js @@ -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}` + ); + } + }; } diff --git a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js index 713ff05..dc79ea4 100644 --- a/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js +++ b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js @@ -1,227 +1,286 @@ -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); + console.log( + `➡ [${this.id}] Closing main page context: ${this.page_context}` + ); + await safeClosePage(this); - 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; - } + await this.onCloseLogin(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 + }; } diff --git a/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js b/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js index e4ace29..9cf982e 100644 --- a/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js +++ b/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js @@ -1,407 +1,526 @@ -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; + } + + 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 + + try { + if (!this.page_context.isClosed()) { + await this.page_context.reload({ waitUntil: "networkidle0" }); + console.log(`🔁 [${this.id}] Reload page in waitForApiResponse`); + } else { + console.log(`⚠️ [${this.id}] Cannot reload, page already closed.`); + } + } catch (error) { + console.error( + `❌ [${this.id}] Error reloading page:`, + error?.message + ); } - // 📌 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 ? String(close_time) : null, - // close_time: close_time && !this.close_time ? String(close_time) : null, - name, - }, - // [], - ['close_time'], + 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 ? String(close_time) : null, + // close_time: close_time && !this.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}` + ); + + if (!this.page_context.isClosed()) { + 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}`); - } - }; + }; } diff --git a/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js b/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js index bda14cc..3796583 100644 --- a/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js +++ b/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js @@ -1,367 +1,441 @@ -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(200); + + // 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 && + !this.page_context.isClosed() + ) { + 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}`); + } + }; } diff --git a/auto-bid-tool/models/pickles.com.au/pickles-api-bid.js b/auto-bid-tool/models/pickles.com.au/pickles-api-bid.js new file mode 100644 index 0000000..d0190b8 --- /dev/null +++ b/auto-bid-tool/models/pickles.com.au/pickles-api-bid.js @@ -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 + }; +} diff --git a/auto-bid-tool/models/pickles.com.au/pickles-product-bid.js b/auto-bid-tool/models/pickles.com.au/pickles-product-bid.js new file mode 100644 index 0000000..6e11b80 --- /dev/null +++ b/auto-bid-tool/models/pickles.com.au/pickles-product-bid.js @@ -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}`); + } + }; +} diff --git a/auto-bid-tool/models/product-bid.js b/auto-bid-tool/models/product-bid.js index 8f845fe..fb8ee24 100644 --- a/auto-bid-tool/models/product-bid.js +++ b/auto-bid-tool/models/product-bid.js @@ -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); } + } } diff --git a/auto-bid-tool/service/app-service.js b/auto-bid-tool/service/app-service.js index ba9b9f8..9044b62 100644 --- a/auto-bid-tool/service/app-service.js +++ b/auto-bid-tool/service/app-service.js @@ -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; }; diff --git a/auto-bid-tool/system/apis/bid.js b/auto-bid-tool/system/apis/bid.js index 92aced5..8bf79e3 100644 --- a/auto-bid-tool/system/apis/bid.js +++ b/auto-bid-tool/system/apis/bid.js @@ -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; + } }; diff --git a/auto-bid-tool/system/apis/notification.js b/auto-bid-tool/system/apis/notification.js index cbf20a5..c048c08 100644 --- a/auto-bid-tool/system/apis/notification.js +++ b/auto-bid-tool/system/apis/notification.js @@ -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; + } }; diff --git a/auto-bid-tool/system/config.js b/auto-bid-tool/system/config.js index 68de7de..5f3dd41 100644 --- a/auto-bid-tool/system/config.js +++ b/auto-bid-tool/system/config.js @@ -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; diff --git a/auto-bid-tool/system/utils.js b/auto-bid-tool/system/utils.js index c1f0118..21512fe 100644 --- a/auto-bid-tool/system/utils.js +++ b/auto-bid-tool/system/utils.js @@ -1,108 +1,147 @@ -import CONSTANTS from './constants.js'; -import fs from 'fs'; -import path from 'path'; -import { updateStatusWork } from './apis/bid.js'; +import CONSTANTS from "./constants.js"; +import fs from "fs"; +import path from "path"; +import { updateStatusWork } from "./apis/bid.js"; export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value)); -export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => { - if (!page || page.isClosed()) return; +export const takeSnapshot = async ( + page, + item, + imageName, + type = CONSTANTS.TYPE_IMAGE.ERRORS +) => { + if (!page || page.isClosed()) return; - try { - const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id - const typeDir = path.join(baseDir, type); // Thư mục con theo type + try { + const baseDir = path.join( + CONSTANTS.ERROR_IMAGES_PATH, + item.type, + String(item.id) + ); // Thư mục theo lot_id + const typeDir = path.join(baseDir, type); // Thư mục con theo type - // Tạo tên file, nếu type === 'work' thì không có timestamp - const fileName = type === CONSTANTS.TYPE_IMAGE.WORK ? `${imageName}.png` : `${imageName}_${new Date().toISOString().replace(/[:.]/g, '-')}.png`; + // Tạo tên file, nếu type === 'work' thì không có timestamp + const fileName = + type === CONSTANTS.TYPE_IMAGE.WORK + ? `${imageName}.png` + : `${imageName}_${new Date().toISOString().replace(/[:.]/g, "-")}.png`; - const filePath = path.join(typeDir, fileName); + const filePath = path.join(typeDir, fileName); - // Kiểm tra và tạo thư mục nếu chưa tồn tại - if (!fs.existsSync(typeDir)) { - fs.mkdirSync(typeDir, { recursive: true }); - console.log(`📂 Save at folder: ${typeDir}`); - } - - // await page.waitForSelector('body', { visible: true, timeout: 5000 }); - // Kiểm tra có thể điều hướng trang không - const isPageResponsive = await page.evaluate(() => document.readyState === 'complete'); - if (!isPageResponsive) { - console.log('🚫 Page is unresponsive, skipping snapshot.'); - return; - } - - // Chờ tối đa 15 giây, nếu không thấy thì bỏ qua - await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => { - console.log('⚠️ Body selector not found, skipping snapshot.'); - return; - }); - - // Chụp ảnh màn hình và lưu vào filePath - await page.screenshot({ path: filePath }); - - console.log(`📸 Image saved at: ${filePath}`); - - // Nếu type === 'work', gửi ảnh lên API - if (type === CONSTANTS.TYPE_IMAGE.WORK) { - await updateStatusWork(item, filePath); - } - } catch (error) { - console.log('Error when snapshot: ' + error.message); + // Kiểm tra và tạo thư mục nếu chưa tồn tại + if (!fs.existsSync(typeDir)) { + fs.mkdirSync(typeDir, { recursive: true }); + console.log(`📂 Save at folder: ${typeDir}`); } + + // await page.waitForSelector('body', { visible: true, timeout: 5000 }); + // Kiểm tra có thể điều hướng trang không + const isPageResponsive = await page.evaluate( + () => document.readyState === "complete" + ); + if (!isPageResponsive) { + console.log("🚫 Page is unresponsive, skipping snapshot."); + return; + } + + // Chờ tối đa 15 giây, nếu không thấy thì bỏ qua + await page + .waitForSelector("body", { visible: true, timeout: 15000 }) + .catch(() => { + console.log("⚠️ Body selector not found, skipping snapshot."); + return; + }); + + // Chụp ảnh màn hình và lưu vào filePath + await page.screenshot({ path: filePath }); + + console.log(`📸 Image saved at: ${filePath}`); + + // Nếu type === 'work', gửi ảnh lên API + if (type === CONSTANTS.TYPE_IMAGE.WORK) { + await updateStatusWork(item, filePath); + } + } catch (error) { + console.log("Error when snapshot: " + error.message); + } }; export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -export async function safeClosePage(item) { - try { - const page = item.page_context; +export const safeClosePageReal = async (page) => { + if (!page) return; - if (!page?.isClosed() && page?.close) { - await page.close(); - } - - item.page_context = undefined; - if (item?.page_context) { - item.page_context = undefined; - } - } catch (error) { - console.log("Can't close item: " + item.id); + try { + if (page.isClosed()) { + console.log(`✅ Page already closed: ${page.url()}`); + return; } + + page.removeAllListeners(); // ✂️ Remove hết listeners trước khi close + await page.close({ runBeforeUnload: true }); // 🛑 Đóng an toàn + console.log(`✅ Successfully closed page: ${page.url()}`); + } catch (err) { + console.warn( + `⚠️ Error closing page ${page.url ? page.url() : ""}: ${err.message}` + ); + } +}; + +export async function safeClosePage(item) { + try { + const page = item.page_context; + + if (!page?.isClosed() && page?.close) { + await safeClosePageReal(page); + // await page.close(); + } + + item.page_context = undefined; + if (item?.page_context) { + item.page_context = undefined; + } + } catch (error) { + console.log("Can't close item: " + item.id); + } } export function isTimeReached(targetTime) { - if (!targetTime) return false; + if (!targetTime) return false; - const targetDate = new Date(targetTime); - const now = new Date(); + const targetDate = new Date(targetTime); + const now = new Date(); - return now >= targetDate; + return now >= targetDate; } export function extractNumber(str) { - const match = str.match(/\d+(\.\d+)?/); - return match ? parseFloat(match[0]) : null; + const match = str.match(/\d+(\.\d+)?/); + return match ? parseFloat(match[0]) : null; } export const sanitizeFileName = (url) => { - return url.replace(/[:\/]/g, '_'); + return url.replace(/[:\/]/g, "_"); }; export const getPathProfile = (origin_url) => { - return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json'); + return path.join( + CONSTANTS.PROFILE_PATH, + sanitizeFileName(origin_url) + ".json" + ); }; export function removeFalsyValues(obj, excludeKeys = []) { - return Object.entries(obj).reduce((acc, [key, value]) => { - if (value || excludeKeys.includes(key)) { - acc[key] = value; - } - return acc; - }, {}); + return Object.entries(obj).reduce((acc, [key, value]) => { + if (value || excludeKeys.includes(key)) { + acc[key] = value; + } + return acc; + }, {}); } export const enableAutoBidMessage = (data) => { - return ` + return ` ⭉ Activate Auto Bid
📌 Product: ${data.name}
🔗 Link: Click here
@@ -112,69 +151,110 @@ export const enableAutoBidMessage = (data) => { }; export function convertAETtoUTC(dateString) { - // Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript) - const monthMap = { - Jan: 0, - Feb: 1, - Mar: 2, - Apr: 3, - May: 4, - Jun: 5, - Jul: 6, - Aug: 7, - Sep: 8, - Oct: 9, - Nov: 10, - Dec: 11, - }; + // Bảng ánh xạ tên tháng sang số (0-11, theo chuẩn JavaScript) + const monthMap = { + Jan: 0, + Feb: 1, + Mar: 2, + Apr: 3, + May: 4, + Jun: 5, + Jul: 6, + Aug: 7, + Sep: 8, + Oct: 9, + Nov: 10, + Dec: 11, + }; - // Tách chuỗi đầu vào - const parts = dateString.match(/(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/); - if (!parts) { - throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'"); - } + // Tách chuỗi đầu vào + const parts = dateString.match( + /(\w+)\s(\d+)\s(\w+)\s(\d+),\s(\d+)\s(PM|AM)\sAET/ + ); + if (!parts) { + throw new Error("Error format: 'Sun 6 Apr 2025, 9 PM AET'"); + } - const [, , day, month, year, hour, period] = parts; + const [, , day, month, year, hour, period] = parts; - // Chuyển đổi giờ sang định dạng 24h - let hours = parseInt(hour, 10); - if (period === 'PM' && hours !== 12) hours += 12; - if (period === 'AM' && hours === 12) hours = 0; + // Chuyển đổi giờ sang định dạng 24h + let hours = parseInt(hour, 10); + if (period === "PM" && hours !== 12) hours += 12; + if (period === "AM" && hours === 12) hours = 0; - // Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ) - const date = new Date(Date.UTC(parseInt(year, 10), monthMap[month], parseInt(day, 10), hours, 0, 0)); + // Tạo đối tượng Date ban đầu (chưa điều chỉnh múi giờ) + const date = new Date( + Date.UTC( + parseInt(year, 10), + monthMap[month], + parseInt(day, 10), + hours, + 0, + 0 + ) + ); - // Hàm kiểm tra DST cho AET - function isDST(date) { - const year = date.getUTCFullYear(); - const month = date.getUTCMonth(); - const day = date.getUTCDate(); + // Hàm kiểm tra DST cho AET + function isDST(date) { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth(); + const day = date.getUTCDate(); - // DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT) - const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10 - dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên - const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10 + // DST bắt đầu: Chủ nhật đầu tiên của tháng 10 (2:00 AM AEST -> 3:00 AM AEDT) + const dstStart = new Date(Date.UTC(year, 9, 1, 0, 0, 0)); // 1/10 + dstStart.setUTCDate(1 + ((7 - dstStart.getUTCDay()) % 7)); // Chủ nhật đầu tiên + const dstStartTime = dstStart.getTime() + 2 * 60 * 60 * 1000; // 2:00 AM UTC+10 - // DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST) - const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4 - dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên - const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11 + // DST kết thúc: Chủ nhật đầu tiên của tháng 4 (3:00 AM AEDT -> 2:00 AM AEST) + const dstEnd = new Date(Date.UTC(year, 3, 1, 0, 0, 0)); // 1/4 + dstEnd.setUTCDate(1 + ((7 - dstEnd.getUTCDay()) % 7)); // Chủ nhật đầu tiên + const dstEndTime = dstEnd.getTime() + 3 * 60 * 60 * 1000; // 3:00 AM UTC+11 - const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10) - return currentTime >= dstStartTime && currentTime < dstEndTime; - } + const currentTime = date.getTime() + 10 * 60 * 60 * 1000; // Thời gian AET (giả định ban đầu UTC+10) + return currentTime >= dstStartTime && currentTime < dstEndTime; + } - // Xác định offset dựa trên DST - const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không + // Xác định offset dựa trên DST + const offset = isDST(date) ? 11 : 10; // UTC+11 nếu DST, UTC+10 nếu không - // Điều chỉnh thời gian về UTC - const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000); + // Điều chỉnh thời gian về UTC + const utcDate = new Date(date.getTime() - offset * 60 * 60 * 1000); - // Trả về chuỗi UTC - return utcDate.toUTCString(); + // Trả về chuỗi UTC + return utcDate.toUTCString(); } export function extractPriceNumber(priceString) { - const cleaned = priceString.replace(/[^\d.]/g, ''); - return parseFloat(cleaned); + const cleaned = priceString.replace(/[^\d.]/g, ""); + return parseFloat(cleaned); +} + +export function findEarlyLoginTime(webBid) { + const now = new Date(); + + // Bước 1: Lọc ra những bid có close_time hợp lệ + const validChildren = webBid.children.filter((child) => child.close_time); + + if (validChildren.length === 0) return null; + + // Bước 2: Tìm bid có close_time gần hiện tại nhất + const closestBid = validChildren.reduce((closest, current) => { + const closestDiff = Math.abs( + new Date(closest.close_time).getTime() - now.getTime() + ); + const currentDiff = Math.abs( + new Date(current.close_time).getTime() - now.getTime() + ); + return currentDiff < closestDiff ? current : closest; + }); + + if (!closestBid.close_time) return null; + + // Bước 3: Tính toán thời gian login sớm + const closeTime = new Date(closestBid.close_time); + closeTime.setSeconds( + closeTime.getSeconds() - (webBid.early_login_seconds || 0) + ); + + return closeTime.toISOString(); }