From 7f2083f25ba244bcb11ac25ebc5508a7e977d75e Mon Sep 17 00:00:00 2001 From: nkhangg Date: Fri, 11 Apr 2025 11:11:51 +0700 Subject: [PATCH] langtons --- auto-bid-admin/src/apis/bid.ts | 16 +- .../src/components/dashboard/working-page.tsx | 34 +- auto-bid-admin/src/pages/bids.tsx | 20 +- auto-bid-admin/src/system/type/index.ts | 1 + auto-bid-server/.vscode/settings.json | 3 + auto-bid-server/a.html | 178 ++++++++ .../bot-data/group_-1002593407119.json | 6 + auto-bid-server/bot-data/metadata.json | 2 +- auto-bid-server/package-lock.json | 196 +++++++++ auto-bid-server/package.json | 5 + .../src/modules/bids/apis/bot-telegram.api.ts | 15 +- .../src/modules/bids/bids.module.ts | 22 +- .../admin/admin-bids.controller.ts | 8 + .../controllers/client/bids.controller.ts | 39 +- .../client/out-bid-logs.controller.ts | 6 + .../bids/dto/bid/client-update-bid.dto.ts | 4 + .../bids/entities/out-bid-log.entity.ts | 2 +- .../entities/send-message-histories.entity.ts | 3 + .../modules/bids/getways/admin-bid-getway.ts | 23 +- .../src/modules/bids/getways/bid-getway.ts | 23 +- .../bids/services/bid-histories.service.ts | 44 +- .../src/modules/bids/services/bids.service.ts | 80 +++- .../bids/services/mail/imap.service.ts | 131 ++++++ .../bids/services/out-bid-logs.service.ts | 23 +- .../modules/bids/services/web-bids.service.ts | 10 +- .../src/modules/bids/utils/events.ts | 13 + .../client-notification.controller.ts | 31 ++ .../controllers/notification.controller.ts | 12 + .../notification/dto/send-message.dto.ts | 9 + .../notification.controller.spec.ts | 2 +- .../notification/notification.controller.ts | 16 - .../notification/notification.module.ts | 15 +- .../notification/notification.service.ts | 24 +- auto-bid-server/src/ultils/index.ts | 73 +++- auto-bid-tool/.vscode/settings.json | 3 + auto-bid-tool/data/fake-out-lot-langtons.json | 29 ++ auto-bid-tool/index.js | 21 +- .../models/grays.com/grays-api-bid.js | 59 ++- .../models/grays.com/grays-product-bid.js | 34 +- .../langtons.com.au/langtons-api-bid.js | 225 ++++++++++ .../langtons.com.au/langtons-product-bid.js | 394 ++++++++++++++++++ .../models/lawsons.com.au/lawsons-api-bid.js | 260 ++++++++++++ .../lawsons.com.au/lawsons-product-bid.js | 114 +++++ auto-bid-tool/models/product-bid.js | 11 +- auto-bid-tool/service/app-service.js | 24 +- auto-bid-tool/system/apis/bid.js | 11 +- auto-bid-tool/system/apis/notification.js | 21 + auto-bid-tool/system/config.js | 17 + auto-bid-tool/system/utils.js | 79 +++- 49 files changed, 2211 insertions(+), 180 deletions(-) create mode 100644 auto-bid-server/.vscode/settings.json create mode 100644 auto-bid-server/a.html create mode 100644 auto-bid-server/src/modules/bids/services/mail/imap.service.ts create mode 100644 auto-bid-server/src/modules/bids/utils/events.ts create mode 100644 auto-bid-server/src/modules/notification/controllers/client-notification.controller.ts create mode 100644 auto-bid-server/src/modules/notification/controllers/notification.controller.ts create mode 100644 auto-bid-server/src/modules/notification/dto/send-message.dto.ts delete mode 100644 auto-bid-server/src/modules/notification/notification.controller.ts create mode 100644 auto-bid-tool/.vscode/settings.json create mode 100644 auto-bid-tool/data/fake-out-lot-langtons.json create mode 100644 auto-bid-tool/models/langtons.com.au/langtons-api-bid.js create mode 100644 auto-bid-tool/models/langtons.com.au/langtons-product-bid.js create mode 100644 auto-bid-tool/models/lawsons.com.au/lawsons-api-bid.js create mode 100644 auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js create mode 100644 auto-bid-tool/system/apis/notification.js diff --git a/auto-bid-admin/src/apis/bid.ts b/auto-bid-admin/src/apis/bid.ts index b951115..f367e72 100644 --- a/auto-bid-admin/src/apis/bid.ts +++ b/auto-bid-admin/src/apis/bid.ts @@ -1,6 +1,6 @@ import { generateNestParams, handleError, handleSuccess } from '.'; import axios from '../lib/axios'; -import { IBid } from '../system/type'; +import { IBid, IWebBid } from '../system/type'; import { removeFalsyValues } from '../utils'; export const getBids = async (params: Record) => { @@ -104,3 +104,17 @@ export const deletesBid = async (bids: IBid[]) => { handleError(error); } }; + +export const getImagesWorking = async (values: (IBid | IWebBid) & { type: string }) => { + try { + const { data } = await axios({ + url: `bids/images-working/${values.type.toLocaleLowerCase().replace('_', '-')}/${values.id}`, + withCredentials: true, + method: 'GET', + }); + + return data; + } catch (error) { + handleError(error); + } +}; diff --git a/auto-bid-admin/src/components/dashboard/working-page.tsx b/auto-bid-admin/src/components/dashboard/working-page.tsx index fbd2194..a3f0d72 100644 --- a/auto-bid-admin/src/components/dashboard/working-page.tsx +++ b/auto-bid-admin/src/components/dashboard/working-page.tsx @@ -3,6 +3,7 @@ import { useDisclosure } from '@mantine/hooks'; import moment from 'moment'; import { useEffect, useState } from 'react'; import { Socket } from 'socket.io-client'; +import { getImagesWorking } from '../../apis/bid'; import { IBid, IWebBid } from '../../system/type'; import ShowImageModal from './show-image-modal'; @@ -16,19 +17,27 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) { const [opened, { open, close }] = useDisclosure(false); - const [imageSrc, setImageSrc] = useState(`${import.meta.env.VITE_BASE_URL}bids/status-working/${data.type.replace('_', '-').toLowerCase()}/${data.id}/working`); + const [imageSrc, setImageSrc] = useState(null); - const [lastUpdate, setLastUpdate] = useState(new Date()); + const [lastUpdate, setLastUpdate] = useState(null); function isIBid(obj: IBid | IWebBid): obj is IBid { return 'name' in obj; } + const renderUrl = ({ type, id }: (IBid | IWebBid) & { type: string }, name: string) => { + return `${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${name}`; + }; + + const extractTime = (filename: string) => { + return Number(filename.split('-')[0]) || 0; + }; + useEffect(() => { const updateImage = ({ type, id, filename }: { type: string; filename: string; id: IBid['id'] }) => { if (type == data.type && id == data.id) { - setLastUpdate(new Date()); - setImageSrc(`${import.meta.env.VITE_BASE_URL}bids/status-working/${type.replace('_', '-').toLowerCase()}/${id}/${filename}`); + setLastUpdate(new Date(extractTime(filename))); + setImageSrc(renderUrl(data, filename)); } }; @@ -37,8 +46,23 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) { return () => { socket.off('working', updateImage); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [socket, data.id, data.type]); + useEffect(() => { + (async () => { + const result = await getImagesWorking(data); + + if (!result || !result.data) return; + + const filename = result.data[0]; + + setImageSrc(renderUrl(data, filename)); + setLastUpdate(new Date(extractTime(filename))); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <> @@ -74,7 +98,7 @@ export default function WorkingPage({ data, socket }: IWorkingPageProps) { - + ); } diff --git a/auto-bid-admin/src/pages/bids.tsx b/auto-bid-admin/src/pages/bids.tsx index 3af0acb..a07fc15 100644 --- a/auto-bid-admin/src/pages/bids.tsx +++ b/auto-bid-admin/src/pages/bids.tsx @@ -243,15 +243,17 @@ export default function Bids() { > Histories - { - setClickData(row); - historiesGraysApiModel.open(); - }} - leftSection={} - > - Bids - + {['https://www.grays.com'].includes(row?.web_bid.origin_url) && ( + { + setClickData(row); + historiesGraysApiModel.open(); + }} + leftSection={} + > + Bids + + )} + + + +Callcentric Email + + + += +
+ + +
Please do not respond to this automated message, you will not receive = +a reply
+
+ + + + + +
+ + +
3D"Callcentric"= +
+
+ + + +
+

Dear Morgan Vu,

You have received an inbound text message from 223= +95 on your DID 17146893350:

Your LANGTONS verific= +ation code is: 012152. This code will expire in 10 minutes.
+ +
+
+ + + + +
+ +Follow us on Twitter at:
+https://twitter.com/Callcentric= + +

+ +If you have any questions/concerns, please contact our Customer Service Tea= +m via the following page:
+https://www.callcen= +tric.com/CustomerService

+ +For instructions on how to unsubscribe from our Newsletters/Service Announc= +ements please visit:
+ +https://www.callcentric= +=2Ecom/faq/11/306

+ +www.callcentric.com

+ +
+
+ + + \ No newline at end of file diff --git a/auto-bid-server/bot-data/group_-1002593407119.json b/auto-bid-server/bot-data/group_-1002593407119.json index da03669..ca5f6f2 100644 --- a/auto-bid-server/bot-data/group_-1002593407119.json +++ b/auto-bid-server/bot-data/group_-1002593407119.json @@ -21,6 +21,12 @@ "can_manage_topics": true }, "join_to_send_messages": true, + "accepted_gift_types": { + "unlimited_gifts": false, + "limited_gifts": false, + "unique_gifts": false, + "premium_subscription": false + }, "max_reaction_count": 11, "accent_color_id": 2 } \ No newline at end of file diff --git a/auto-bid-server/bot-data/metadata.json b/auto-bid-server/bot-data/metadata.json index 5695ff7..b0f60df 100644 --- a/auto-bid-server/bot-data/metadata.json +++ b/auto-bid-server/bot-data/metadata.json @@ -1 +1 @@ -{"createdAt":1743133400720} \ No newline at end of file +{"createdAt":1744342271961} \ No newline at end of file diff --git a/auto-bid-server/package-lock.json b/auto-bid-server/package-lock.json index 6a44c92..e0374c4 100644 --- a/auto-bid-server/package-lock.json +++ b/auto-bid-server/package-lock.json @@ -27,12 +27,15 @@ "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", + "imap": "^0.8.19", + "lodash": "^4.17.21", "multer": "^1.4.5-lts.1", "mysql2": "^3.13.0", "nestjs-paginate": "^11.1.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sharp": "^0.33.5", + "simple-imap": "^0.1.6", "typeorm": "^0.3.21", "uuid": "^11.1.0" }, @@ -44,7 +47,9 @@ "@types/cookie": "^0.6.0", "@types/cookie-parser": "^1.4.8", "@types/express": "^4.17.17", + "@types/imap": "^0.8.42", "@types/jest": "^29.5.2", + "@types/lodash": "^4.17.16", "@types/multer": "^1.4.12", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", @@ -2931,6 +2936,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/imap": { + "version": "0.8.42", + "resolved": "https://registry.npmjs.org/@types/imap/-/imap-0.8.42.tgz", + "integrity": "sha512-FusePG9Cp2GYN6OLow9xBCkjznFkAR7WCz0Fm+j1p/ER6C8V8P71DtjpSmwrZsS7zekCeqdTPHEk9N5OgPwcsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2985,6 +3000,13 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3558,6 +3580,12 @@ "node": ">=0.4.0" } }, + "node_modules/addressparser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", + "integrity": "sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==", + "license": "MIT" + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -5189,6 +5217,27 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/engine.io": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", @@ -6771,6 +6820,42 @@ "node": ">= 4" } }, + "node_modules/imap": { + "version": "0.8.19", + "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz", + "integrity": "sha512-z5DxEA1uRnZG73UcPA4ES5NSCGnPuuouUx43OPX7KZx1yzq3N8/vx2mtXEShT5inxB3pRgnfG1hijfu7XN2YMw==", + "dependencies": { + "readable-stream": "1.1.x", + "utf7": ">=1.0.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/imap/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/imap/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/imap/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -8263,6 +8348,18 @@ "node": ">=12" } }, + "node_modules/mailparser": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-0.4.6.tgz", + "integrity": "sha512-G9D9e+v0UKpG7jiANGNkMCinqcG+Q70GngQQm8WknkMEFLx7Cyp+vnoAJaOtVCR9dPuyVAKaUJlOgDE6HTwmSw==", + "deprecated": "Mailparser versions older than v2.3.0 are deprecated", + "dependencies": { + "encoding": ">=0.1.4", + "mime": "*", + "mimelib": ">=0.2.17", + "uue": "~1.0.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8422,6 +8519,17 @@ "node": ">= 0.6" } }, + "node_modules/mimelib": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/mimelib/-/mimelib-0.3.1.tgz", + "integrity": "sha512-ipzNp6TBsNfD3hButGlPVlGmuCgybIM9SBf8YwIG+SYmBgtU0u8wjf+BSrJX0mvqtv59SLmwphw/XiCbkLWv7w==", + "deprecated": "This project is unmaintained", + "license": "EUPL-1.1", + "dependencies": { + "addressparser": "~1.0.1", + "encoding": "~0.1.12" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -8509,6 +8617,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.8.3.tgz", + "integrity": "sha512-BV9tebhfgcO0mT3desIQEM8vdFGOsqwfJ71a+mz/ECVa0uiokosBq3A3Vyk7XhyNUzzQECX/T/bpHQANemf5Vg==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10128,6 +10244,64 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-imap": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/simple-imap/-/simple-imap-0.1.6.tgz", + "integrity": "sha512-mdmaq1u/qEOa8Ow3ZPKv2KXZbV5oHfFewqgqi8FnxD0mQzMzMLtKcE3TEmI7qhp1FmRcdqBlYeUMtOt7nS7XkA==", + "license": "MIT", + "dependencies": { + "imap": "0.8.14", + "mailparser": "0.4.6", + "moment": "2.8.3" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/simple-imap/node_modules/imap": { + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/imap/-/imap-0.8.14.tgz", + "integrity": "sha512-8opGClGwiZ7/NBc3wofk7PbGkaLqggXx8W0D/Bc6YQdufl9CixmMBKm5rrn9gyelMVdMQVfpXp3D6gdXBgtAdg==", + "dependencies": { + "readable-stream": "1.1.x", + "utf7": "1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/simple-imap/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/simple-imap/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/simple-imap/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/simple-imap/node_modules/utf7": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utf7/-/utf7-1.0.0.tgz", + "integrity": "sha512-vMfAo9fTQeZG7W8PEOWa9g++rkHxYqBBrb22OWY+OMUnSFFtQURySIIJ2LstpF5JI6SzsGQNaglin2aLwA7pTA==", + "engines": { + "node": "*" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -11371,6 +11545,23 @@ "punycode": "^2.1.0" } }, + "node_modules/utf7": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utf7/-/utf7-1.0.2.tgz", + "integrity": "sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==", + "dependencies": { + "semver": "~5.3.0" + } + }, + "node_modules/utf7/node_modules/semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11386,6 +11577,11 @@ "node": ">= 0.4.0" } }, + "node_modules/uue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uue/-/uue-1.0.0.tgz", + "integrity": "sha512-gEwvMeDsv0Q4SFsjhp/PmsXizTfEn3B8SXqtRiz5jE2nsRgEBCY5W5gmxOFrPVzbnDCELwOKyMnOEoL/W/TG7w==" + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/auto-bid-server/package.json b/auto-bid-server/package.json index 76fed1b..ec5cb1d 100644 --- a/auto-bid-server/package.json +++ b/auto-bid-server/package.json @@ -43,12 +43,15 @@ "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "dayjs": "^1.11.13", + "imap": "^0.8.19", + "lodash": "^4.17.21", "multer": "^1.4.5-lts.1", "mysql2": "^3.13.0", "nestjs-paginate": "^11.1.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sharp": "^0.33.5", + "simple-imap": "^0.1.6", "typeorm": "^0.3.21", "uuid": "^11.1.0" }, @@ -60,7 +63,9 @@ "@types/cookie": "^0.6.0", "@types/cookie-parser": "^1.4.8", "@types/express": "^4.17.17", + "@types/imap": "^0.8.42", "@types/jest": "^29.5.2", + "@types/lodash": "^4.17.16", "@types/multer": "^1.4.12", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", diff --git a/auto-bid-server/src/modules/bids/apis/bot-telegram.api.ts b/auto-bid-server/src/modules/bids/apis/bot-telegram.api.ts index bfa2538..31766e3 100644 --- a/auto-bid-server/src/modules/bids/apis/bot-telegram.api.ts +++ b/auto-bid-server/src/modules/bids/apis/bot-telegram.api.ts @@ -1,22 +1,11 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; -import { escapeMarkdownV2 } from 'src/ultils'; -import { Bid } from '../entities/bid.entity'; import * as dayjs from 'dayjs'; -import { SendMessageHistoriesService } from '../services/send-message-histories.service'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; +import { Bid } from '../entities/bid.entity'; import { Constant } from '../utils/constant'; -import { - existsSync, - mkdir, - mkdirSync, - readdirSync, - readFileSync, - statSync, - writeFile, - writeFileSync, -} from 'fs'; @Injectable() export class BotTelegramApi { diff --git a/auto-bid-server/src/modules/bids/bids.module.ts b/auto-bid-server/src/modules/bids/bids.module.ts index a77d433..206a708 100644 --- a/auto-bid-server/src/modules/bids/bids.module.ts +++ b/auto-bid-server/src/modules/bids/bids.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; -import { EventEmitterModule } from '@nestjs/event-emitter'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminsModule } from '../admins/admins.module'; +import { NotificationModule } from '../notification/notification.module'; +import { BotTelegramApi } from './apis/bot-telegram.api'; +import { GraysApi } from './apis/grays.api'; import { AdminBidHistoriesController } from './controllers/admin/admin-bid-histories.controller'; import { AdminBidsController } from './controllers/admin/admin-bids.controller'; import { AdminOutBidLogsController } from './controllers/admin/admin-out-bid-logs.controller'; +import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller'; import { AdminWebBidsController } from './controllers/admin/admin-web-bids.controller'; import { BidHistoriesController } from './controllers/client/bid-histories.controller'; import { BidsController } from './controllers/client/bids.controller'; @@ -11,21 +15,16 @@ import { OutBidLogsController } from './controllers/client/out-bid-logs.controll import { BidHistory } from './entities/bid-history.entity'; import { Bid } from './entities/bid.entity'; import { OutBidLog } from './entities/out-bid-log.entity'; +import { SendMessageHistory } from './entities/send-message-histories.entity'; import { WebBid } from './entities/wed-bid.entity'; +import { AdminBidGateway } from './getways/admin-bid-getway'; import { BidGateway } from './getways/bid-getway'; import { BidHistoriesService } from './services/bid-histories.service'; import { BidsService } from './services/bids.service'; +import { ImapService } from './services/mail/imap.service'; import { OutBidLogsService } from './services/out-bid-logs.service'; -import { WebBidsService } from './services/web-bids.service'; -import { BotTelegramApi } from './apis/bot-telegram.api'; -import { GraysApi } from './apis/grays.api'; -import { SendMessageHistory } from './entities/send-message-histories.entity'; import { SendMessageHistoriesService } from './services/send-message-histories.service'; -import { AdminSendMessageHistoriesController } from './controllers/admin/admin-send-message-histories.controller'; -import { AuthModule } from '../auth/auth.module'; -import { AdminsModule } from '../admins/admins.module'; -import { AdminBidGateway } from './getways/admin-bid-getway'; -import { NotificationModule } from '../notification/notification.module'; +import { WebBidsService } from './services/web-bids.service'; @Module({ imports: [ @@ -60,7 +59,8 @@ import { NotificationModule } from '../notification/notification.module'; BotTelegramApi, GraysApi, SendMessageHistoriesService, + ImapService, ], - exports: [BotTelegramApi], + exports: [BotTelegramApi, SendMessageHistoriesService, BidsService], }) export class BidsModule {} diff --git a/auto-bid-server/src/modules/bids/controllers/admin/admin-bids.controller.ts b/auto-bid-server/src/modules/bids/controllers/admin/admin-bids.controller.ts index 43fc7e0..ca6021e 100644 --- a/auto-bid-server/src/modules/bids/controllers/admin/admin-bids.controller.ts +++ b/auto-bid-server/src/modules/bids/controllers/admin/admin-bids.controller.ts @@ -59,4 +59,12 @@ export class AdminBidsController { ) { return await this.bidsService.getStatusWorkingImage(id, type, name, res); } + + @Get('images-working/:type/:id') + async getImagesWorking( + @Param('id') id: Bid['id'], + @Param('type') type: string, + ) { + return await this.bidsService.getImagesWorking(id, type); + } } 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 550398b..5894a44 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 @@ -5,22 +5,30 @@ import { Param, Post, Put, - Res, UploadedFile, UseInterceptors, } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { plainToClass } from 'class-transformer'; +import { memoryStorage } from 'multer'; import { ClientUpdateBidDto } from '../../dto/bid/client-update-bid.dto'; import { CreateBidDto } from '../../dto/bid/create-bid.dto'; -import { Bid } from '../../entities/bid.entity'; -import { BidsService } from '../../services/bids.service'; import { UpdateStatusByPriceDto } from '../../dto/bid/update-status-by-price.dto'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { memoryStorage } from 'multer'; -import { Response } from 'express'; +import { Bid } from '../../entities/bid.entity'; +import { WebBid } from '../../entities/wed-bid.entity'; +import { BidsService } from '../../services/bids.service'; +import { WebBidsService } from '../../services/web-bids.service'; +import { Event } from '../../utils/events'; +import AppResponse from '@/response/app-response'; @Controller('bids') export class BidsController { - constructor(private readonly bidsService: BidsService) {} + constructor( + private readonly bidsService: BidsService, + private readonly eventEmitter: EventEmitter2, + private readonly webBidService: WebBidsService, + ) {} @Get() index() { @@ -59,4 +67,21 @@ export class BidsController { ) { return this.bidsService.updateStatusWork(id, type, image); } + + @Post('test') + async test(@Body('code') code: string) { + const webBid = await this.webBidService.webBidRepo.findOne({ + // where: { id: 9 }, + where: { id: 8 }, + }); + + this.eventEmitter.emit(Event.verifyCode(webBid), { + code, + // name: 'LAWSONS', + name: 'LANGTONS', + web_bid: plainToClass(WebBid, webBid), + }); + + return AppResponse.toResponse({ code }); + } } diff --git a/auto-bid-server/src/modules/bids/controllers/client/out-bid-logs.controller.ts b/auto-bid-server/src/modules/bids/controllers/client/out-bid-logs.controller.ts index cd54530..09af52f 100644 --- a/auto-bid-server/src/modules/bids/controllers/client/out-bid-logs.controller.ts +++ b/auto-bid-server/src/modules/bids/controllers/client/out-bid-logs.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Post } from '@nestjs/common'; import { CreateOutBidLogsDto } from '../../dto/out-bid-log/create-out-bid-logs.dto'; import { OutBidLogsService } from '../../services/out-bid-logs.service'; +import { CreateOutBidLogDto } from '../../dto/out-bid-log/create-out-bid-log.dto'; @Controller('out-bid-logs') export class OutBidLogsController { @@ -10,4 +11,9 @@ export class OutBidLogsController { create(@Body() data: CreateOutBidLogsDto) { return this.outBidLogsService.create(data.data); } + + @Post('simple') + createSimple(@Body() data: CreateOutBidLogDto) { + return this.outBidLogsService.createSimple(data); + } } diff --git a/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts b/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts index d79e3d5..7059790 100644 --- a/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts +++ b/auto-bid-server/src/modules/bids/dto/bid/client-update-bid.dto.ts @@ -5,6 +5,10 @@ export class ClientUpdateBidDto { @IsOptional() lot_id: string; + @IsString() + @IsOptional() + model: string; + @IsOptional() @IsString() close_time: string | null; diff --git a/auto-bid-server/src/modules/bids/entities/out-bid-log.entity.ts b/auto-bid-server/src/modules/bids/entities/out-bid-log.entity.ts index 715a2d8..ed8a827 100644 --- a/auto-bid-server/src/modules/bids/entities/out-bid-log.entity.ts +++ b/auto-bid-server/src/modules/bids/entities/out-bid-log.entity.ts @@ -16,6 +16,6 @@ export class OutBidLog extends Timestamp { @Column() out_price: number; - @Column({ default: null, nullable: true }) + @Column({ default: null, nullable: true, type: 'text' }) raw_data: string; } diff --git a/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts b/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts index a691d88..4afe19c 100644 --- a/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts +++ b/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts @@ -10,6 +10,9 @@ export class SendMessageHistory extends Timestamp { @Column({ default: null, nullable: true, type: 'text' }) message: string; + @Column({ default: 'biding' }) + type: string; + @ManyToOne(() => Bid, (bid) => bid.sendMessageHistories, { onDelete: 'CASCADE', }) 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 3ba98bc..1dec825 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 @@ -11,6 +11,10 @@ import { plainToClass } from 'class-transformer'; import { Server, Socket } from 'socket.io'; import { WebBid } from '../entities/wed-bid.entity'; import { WebBidsService } from '../services/web-bids.service'; +import * as Imap from 'imap'; +import { ImapService } from '../services/mail/imap.service'; +import { ConfigService } from '@nestjs/config'; +import { Event } from '../utils/events'; @WebSocketGateway({ namespace: 'admin-bid-ws', @@ -29,16 +33,22 @@ export class AdminBidGateway implements OnGatewayConnection { private readonly jwtService: JwtService, private webBidsService: WebBidsService, private readonly adminService: AdminsService, - ) {} + private readonly imapService: ImapService, + ) { + this.imapService.server = this.server; + } async onModuleInit() { - this.eventEmitter.on('bids.updated', (data) => { - this.server.emit('adminBidsUpdated', plainToClass(WebBid, data)); + this.eventEmitter.on(Event.BIDS_UPDATED, (data) => { + this.server.emit(Event.ADMIN_BIDS_UPDATED, plainToClass(WebBid, data)); }); - this.eventEmitter.on('working', (data) => { - this.server.emit('working', data); + this.eventEmitter.on(Event.WORKING, (data) => { + this.server.emit(Event.WORKING, data); }); + + // IMAP + this.imapService.connectIMAP(); } async handleConnection(client: Socket) { @@ -47,7 +57,8 @@ export class AdminBidGateway implements OnGatewayConnection { console.log(`📢 Client connected: ${client.id}`); const data = await this.webBidsService.getDataClient(); + // Gửi dữ liệu bids ngay khi client kết nối - client.emit('adminBidsUpdated', plainToClass(WebBid, data)); + client.emit(Event.ADMIN_BIDS_UPDATED, plainToClass(WebBid, data)); } } diff --git a/auto-bid-server/src/modules/bids/getways/bid-getway.ts b/auto-bid-server/src/modules/bids/getways/bid-getway.ts index 391097e..4da0a0d 100644 --- a/auto-bid-server/src/modules/bids/getways/bid-getway.ts +++ b/auto-bid-server/src/modules/bids/getways/bid-getway.ts @@ -9,6 +9,8 @@ import { import { Server, Socket } from 'socket.io'; import { BidsService } from '../services/bids.service'; import { WebBidsService } from '../services/web-bids.service'; +import { Event } from '../utils/events'; +import { WebBid } from '../entities/wed-bid.entity'; @WebSocketGateway({ namespace: 'bid-ws', @@ -29,13 +31,24 @@ export class BidGateway implements OnGatewayConnection { ) {} async onModuleInit() { - this.eventEmitter.on('bids.updated', (data) => { - this.server.emit('bidsUpdated', data); + this.eventEmitter.on(Event.BIDS_UPDATED, (data) => { + this.server.emit(Event.BIDS_UPDATED, data); }); - this.eventEmitter.on('web.updated', (data) => { - this.server.emit('webUpdated', data); + this.eventEmitter.on(Event.WEB_UPDATED, (data) => { + this.server.emit(Event.WEB_UPDATED, data); }); + + this.eventEmitter.onAny( + ( + event: string, + payload: { name: string; code: string; web_bid: WebBid }, + ) => { + if (!event.startsWith(Event.VERIFY_CODE)) return; + + this.server.emit(Event.verifyCode(payload.web_bid), payload); + }, + ); } async handleConnection(client: Socket) { @@ -45,6 +58,6 @@ export class BidGateway implements OnGatewayConnection { const data = await this.webBidsService.getDataClient(); // Gửi dữ liệu bids ngay khi client kết nối - client.emit('bidsUpdated', data); + client.emit(Event.BIDS_UPDATED, data); } } diff --git a/auto-bid-server/src/modules/bids/services/bid-histories.service.ts b/auto-bid-server/src/modules/bids/services/bid-histories.service.ts index f581bb8..a38279b 100644 --- a/auto-bid-server/src/modules/bids/services/bid-histories.service.ts +++ b/auto-bid-server/src/modules/bids/services/bid-histories.service.ts @@ -14,17 +14,20 @@ import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto'; import { BotTelegramApi } from '../apis/bot-telegram.api'; import { SendMessageHistoriesService } from './send-message-histories.service'; import { NotificationService } from '@/modules/notification/notification.service'; +import { isTimeReached } from '@/ultils'; +import { BidsService } from './bids.service'; @Injectable() export class BidHistoriesService { constructor( @InjectRepository(BidHistory) readonly bidHistoriesRepo: Repository, - @InjectRepository(Bid) - readonly bidsRepo: Repository, + // @InjectRepository(Bid) + // readonly bidsRepo: Repository, private readonly botTelegramApi: BotTelegramApi, readonly sendMessageHistoriesService: SendMessageHistoriesService, private readonly notificationService: NotificationService, + private readonly bidsService: BidsService, ) {} async index() { @@ -32,8 +35,12 @@ export class BidHistoriesService { } async create({ price, bid_id }: CreateBidHistoryDto) { - const bid = await this.bidsRepo.findOne({ where: { id: bid_id } }); + // Tìm thông tin bid từ database + const bid = await this.bidsService.bidsRepo.findOne({ + where: { id: bid_id }, + }); + // Nếu không tìm thấy bid, trả về lỗi 404 if (!bid) throw new NotFoundException( AppResponse.toResponse(null, { @@ -42,23 +49,34 @@ export class BidHistoriesService { }), ); + // Lấy lịch sử đặt giá cao nhất trước đó của bid hiện tại const lastHistory = await this.bidHistoriesRepo.findOne({ where: { bid: { id: bid_id } }, order: { price: 'desc' }, }); + // Nếu đã có lịch sử và giá mới giống với giá cao nhất hiện tại if (lastHistory && lastHistory.price === price) { + // Nếu đã hết thời gian đấu giá, cập nhật trạng thái là 'win-bid' + if (isTimeReached(bid.close_time)) { + this.bidsService.bidsRepo.update(bid_id, { status: 'win-bid' }); + } + + // Ném lỗi không cho đặt giá trùng lặp throw new BadRequestException( AppResponse.toResponse(null, { message: 'Duplicate place bid' }), ); } + // Nếu tổng giá vượt quá mức tối đa cho phép if (price + bid.plus_price > bid.max_price) { - this.bidsRepo.update(bid_id, { status: 'out-bid' }); + // Cập nhật trạng thái bid là 'out-bid' + this.bidsService.bidsRepo.update(bid_id, { status: 'out-bid' }); - // send message event + // Gửi thông báo trạng thái mới qua service thông báo this.notificationService.emitBidStatus({ ...bid, status: 'out-bid' }); + // Ném lỗi không cho đặt giá vượt mức throw new BadRequestException( AppResponse.toResponse(null, { message: 'Price is more than Max price ' + bid.max_price, @@ -66,8 +84,10 @@ export class BidHistoriesService { ); } + // Lưu lịch sử đặt giá mới await this.bidHistoriesRepo.save({ bid, price }); + // Lấy danh sách tất cả lịch sử đặt giá theo thứ tự mới nhất const response = await this.bidHistoriesRepo.find({ where: { bid: { id: bid_id } }, order: { @@ -75,19 +95,29 @@ export class BidHistoriesService { }, }); + // Nếu đây là lần đặt giá đầu tiên, cập nhật cờ `first_bid` thành false if (response.length === 1) { - this.bidsRepo.update(bid_id, { first_bid: false }); + this.bidsService.bidsRepo.update(bid_id, { first_bid: false }); } + // Gửi thông tin bid đến bot telegram const botData = { ...bid, histories: response }; - this.botTelegramApi.sendBidInfo(botData); + // Lưu message đã gửi để theo dõi this.sendMessageHistoriesService.sendMessageRepo.save({ message: this.botTelegramApi.formatBidMessage(botData), bid, }); + // Kiểm tra nếu trạng thái bid thay đổi sau khi lưu, phát sự kiện cập nhật tất cả bid + const bidUpdated = await this.bidsService.bidsRepo.findOne({ + where: { id: bid_id }, + }); + + this.bidsService.emitAllBidEvent(); + + // Trả về danh sách lịch sử đặt giá đã cập nhật return AppResponse.toResponse(plainToClass(BidHistory, response)); } } 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 99f5437..401edf0 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -30,6 +30,7 @@ import { ImageCompressionPipe } from '../pipes/image-compression-pipe'; import { Constant } from '../utils/constant'; import { WebBidsService } from './web-bids.service'; import { NotificationService } from '@/modules/notification/notification.service'; +import { Event } from '../utils/events'; @Injectable() export class BidsService { @@ -76,6 +77,7 @@ export class BidsService { maxLimit: 100, relations: { histories: true, + web_bid: true, }, }); @@ -99,6 +101,7 @@ export class BidsService { const model = extractModelId(data.url); const prev = await this.bidsRepo.findOne({ where: { model: model } }); + if (prev) { throw new BadRequestException( AppResponse.toResponse(null, { message: 'Product is already exits' }), @@ -197,10 +200,20 @@ export class BidsService { async clientUpdate( id: Bid['id'], - { close_time, ...data }: ClientUpdateBidDto, + { close_time, model, ...data }: ClientUpdateBidDto, // Nhận dữ liệu cập nhật ) { - const bid = await this.bidsRepo.findOne({ where: { id } }); + // Tìm kiếm phiên đấu giá trong database theo id + const bid = await this.bidsRepo.findOne({ + where: { id }, + relations: { histories: true }, + order: { + histories: { + price: 'DESC', + }, + }, + }); + // Nếu không tìm thấy phiên đấu giá, trả về lỗi 404 if (!bid) throw new NotFoundException( AppResponse.toResponse(null, { @@ -209,18 +222,34 @@ export class BidsService { }), ); + // Nếu phiên đấu giá chưa có thời gian bắt đầu và kết thúc 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); } - if ( - data.current_price >= bid.max_price + bid.plus_price || - (bid.close_time && isTimeReached(bid.close_time)) - ) { - bid.status = 'out-bid'; + // Kiểm tra nếu thời gian đóng bid đã đạt tới (tức phiên đấu giá đã kết thúc) + if (bid.close_time && isTimeReached(bid.close_time)) { + const bidHistoriesItem = bid.histories[0]; // Lấy lịch sử bid gần nhất (mới nhất) + + if ( + // Nếu giá cuối cùng không phải là giá của người dùng và giá hiện tại vượt quá mức người dùng đặt + bước giá + bidHistoriesItem.price !== data.current_price && + data.current_price > bid.max_price + bid.plus_price + ) { + bid.status = 'out-bid'; // Người dùng đã bị outbid khi đấu giá kết thúc + } else { + bid.status = 'win-bid'; // Người dùng là người thắng nếu không bị outbid + } + } else { + // Nếu phiên đấu giá vẫn đang diễn ra và giá hiện tại vượt quá giới hạn đặt của người dùng + if (data.current_price > bid.max_price + bid.plus_price) { + bid.status = 'out-bid'; // Gán trạng thái là đã bị outbid + } } + // Cập nhật thời gian kết thúc đấu giá nếu `close_time` mới lớn hơn `close_time` cũ if ( close_time && new Date(close_time).getTime() > new Date(bid.close_time).getTime() @@ -228,19 +257,28 @@ export class BidsService { bid.close_time = close_time; } + // Nếu chưa có `model` nhưng dữ liệu mới có model, thì cập nhật model + if (model && !bid.model) { + bid.model = model; + } + + // Lưu cập nhật vào database const result = await this.bidsRepo.save({ ...bid, ...data, - updated_at: new Date(), + updated_at: new Date(), // Cập nhật timestamp }); + // Phát sự kiện cập nhật toàn bộ danh sách đấu giá this.emitAllBidEvent(); // send event message - if (result.status === 'out-bid') { + // Nếu trạng thái của bid là 'out-bid', gửi thông báo + if (['out-bid', 'win-bid'].includes(result.status)) { this.notificationService.emitBidStatus(result); } + // Trả về kết quả cập nhật dưới dạng response chuẩn return AppResponse.toResponse(plainToClass(Bid, result)); } @@ -408,7 +446,7 @@ export class BidsService { let filePath: string; - if (name === 'working') { + if (name === Event.WORKING) { const files = readdirSync(folderPath).filter((file) => /\.(jpg|jpeg|png|webp)$/i.test(file), ); @@ -440,4 +478,26 @@ export class BidsService { return res.sendFile(filePath); } + + async getImagesWorking(id: Bid['id'], type: string) { + const rootDir = process.cwd(); + const folderPath = join( + rootDir, + `${Constant.MEDIA_PATH}/${Constant.WORK_IMAGES_FOLDER}/${type}/${id}`, + ); + + if (!existsSync(folderPath)) { + throw new NotFoundException( + AppResponse.toResponse(null, { + message: 'Folder not found', + status_code: HttpStatus.NOT_FOUND, + }), + ); + } + + // Lấy danh sách file trong folderPath + const files = readdirSync(folderPath); + + return AppResponse.toResponse(files); + } } diff --git a/auto-bid-server/src/modules/bids/services/mail/imap.service.ts b/auto-bid-server/src/modules/bids/services/mail/imap.service.ts new file mode 100644 index 0000000..bca5581 --- /dev/null +++ b/auto-bid-server/src/modules/bids/services/mail/imap.service.ts @@ -0,0 +1,131 @@ +import { extractVerifyCodeLANGTONS, verifyCode } from '@/ultils'; +import { ConfigService } from '@nestjs/config'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { WebSocketGateway } from '@nestjs/websockets'; +import * as Imap from 'imap'; +import { Server, Socket } from 'socket.io'; +import { WebBidsService } from '../web-bids.service'; +import { Like } from 'typeorm'; +import { Event } from '../../utils/events'; +import { plainToClass } from 'class-transformer'; +import { WebBid } from '../../entities/wed-bid.entity'; +@WebSocketGateway({ + namespace: 'admin-bid-ws', + cors: { + origin: '*', + methods: ['GET', 'POST'], + credentials: true, + }, +}) +export class ImapService { + private imap: Imap; + + server: Server; + + constructor( + private readonly configService: ConfigService, + private readonly webBidsService: WebBidsService, + private eventEmitter: EventEmitter2, + ) { + this.imap = new Imap({ + user: this.configService.get('MAIL_USER'), + password: this.configService.get('MAIL_PASSWORD'), + host: this.configService.get('MAIL_SERVER'), + port: 993, + tls: true, + tlsOptions: { rejectUnauthorized: false }, + }); + } + + async connectIMAP() { + this.imap.once('ready', () => { + console.log('📥 IMAP Connected. Listening for new emails...'); + this.openInbox(); + }); + + this.imap.on('error', (err) => { + console.error('❌ IMAP Error:', err); + }); + + this.imap.on('end', () => { + console.log('📴 IMAP Connection Ended'); + setTimeout(() => this.connectIMAP(), 5000); + }); + + this.imap.connect(); + } + + private openInbox() { + this.imap.openBox('INBOX', false, (err, box) => { + if (err) { + console.error('❌ Error opening inbox:', err); + return; + } + + console.log('📬 INBOX opened, waiting for new emails...'); + + // Lắng nghe email mới + this.imap.on('mail', (numNewMsgs: number) => { + console.log(`📩 New email received! (${numNewMsgs} new messages)`); + this.fetchLatestEmail(); + }); + }); + } + + private fetchLatestEmail() { + this.imap.search(['UNSEEN'], (err, results) => { + if (err || !results.length) { + console.log('📭 No new unread emails.'); + return; + } + + const fetchOptions = { bodies: '', struct: true }; + const f = this.imap.fetch(results.slice(-1), fetchOptions); // Lấy email mới nhất + + f.on('message', (msg, seqno) => { + console.log(`📥 Fetching email #${seqno}`); + let emailContent = ''; + let header = ''; + + msg.on('body', (stream) => { + stream.on('data', (chunk) => { + emailContent += chunk.toString(); + }); + + stream.on('end', async () => { + const result = verifyCode(emailContent); + + if (!result) { + throw new Error('fetchLatestEmail: Name or Code is empty'); + } + + const { code, name } = result; + + const webBid = await this.webBidsService.webBidRepo.findOne({ + where: { origin_url: Like(`%${name.toLocaleLowerCase()}%`) }, + }); + + if (!webBid) { + throw new Error('Not found web bid'); + } + + // send message event + this.eventEmitter.emit(Event.verifyCode(webBid), { + code, + name, + web_bid: plainToClass(WebBid, webBid), + }); + }); + }); + }); + + f.on('error', (err) => { + console.error('❌ Fetch error:', err); + }); + + f.on('end', () => { + console.log('✅ Done fetching emails.'); + }); + }); + } +} diff --git a/auto-bid-server/src/modules/bids/services/out-bid-logs.service.ts b/auto-bid-server/src/modules/bids/services/out-bid-logs.service.ts index 0766b5d..0f8db20 100644 --- a/auto-bid-server/src/modules/bids/services/out-bid-logs.service.ts +++ b/auto-bid-server/src/modules/bids/services/out-bid-logs.service.ts @@ -53,11 +53,30 @@ export class OutBidLogsService { skipUpdateIfNoValuesChanged: true, }); - if (!result) throw new BadRequestException(false); + if (!result) throw new BadRequestException(AppResponse.toResponse(false)); return AppResponse.toResponse(true); } catch (error) { - throw new BadRequestException(false); + throw new BadRequestException(AppResponse.toResponse(false)); } } + + async createSimple({ model, lot_id, ...data }: CreateOutBidLogDto) { + const prev = await this.outbidLogRepo.findOne({ + where: { lot_id: lot_id, model: model }, + }); + + if (!prev) { + const result = await this.outbidLogRepo.save({ ...data, lot_id, model }); + + return AppResponse.toResponse(!!result); + } + + const result = await this.outbidLogRepo.update( + { model: model, lot_id: lot_id }, + { ...data }, + ); + + return AppResponse.toResponse(!!result); + } } 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 7878bbd..96e028f 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 @@ -21,6 +21,7 @@ import { extractDomain } from 'src/ultils'; import { Bid } from '../entities/bid.entity'; import { CreateWebBidDto } from '../dto/web-bid/create-web-bid'; import { UpdateWebBidDto } from '../dto/web-bid/update-web-bid'; +import { Event } from '../utils/events'; @Injectable() export class WebBidsService { @@ -73,13 +74,16 @@ export class WebBidsService { async emitAllBidEvent() { const data = await this.getDataClient(); - this.eventEmitter.emit('bids.updated', data); + this.eventEmitter.emit(Event.BIDS_UPDATED, data); } async emitAccountUpdate(id: WebBid['id']) { - const data = await this.webBidRepo.findOne({ where: { id } }); + const data = await this.webBidRepo.findOne({ + where: { id, children: { status: 'biding' } }, + relations: { children: true }, + }); - this.eventEmitter.emit('web.updated', data || null); + this.eventEmitter.emit(Event.WEB_UPDATED, data || null); } async createByUrl(url: string) { diff --git a/auto-bid-server/src/modules/bids/utils/events.ts b/auto-bid-server/src/modules/bids/utils/events.ts new file mode 100644 index 0000000..07df96c --- /dev/null +++ b/auto-bid-server/src/modules/bids/utils/events.ts @@ -0,0 +1,13 @@ +import { WebBid } from '../entities/wed-bid.entity'; + +export class Event { + public static VERIFY_CODE = 'verify-code'; + public static WORKING = 'working'; + public static BIDS_UPDATED = 'bidsUpdated'; + public static ADMIN_BIDS_UPDATED = 'adminBidsUpdated'; + public static WEB_UPDATED = 'webUpdated'; + + public static verifyCode(data: WebBid) { + return `${this.VERIFY_CODE}.${data.origin_url}`; + } +} diff --git a/auto-bid-server/src/modules/notification/controllers/client-notification.controller.ts b/auto-bid-server/src/modules/notification/controllers/client-notification.controller.ts new file mode 100644 index 0000000..93e46fa --- /dev/null +++ b/auto-bid-server/src/modules/notification/controllers/client-notification.controller.ts @@ -0,0 +1,31 @@ +import { BotTelegramApi } from '@/modules/bids/apis/bot-telegram.api'; +import { Body, Controller, Get, Post } from '@nestjs/common'; +import { SendMessageDto } from '../dto/send-message.dto'; +import { NotificationService } from '../notification.service'; +import { BidsService } from '@/modules/bids/services/bids.service'; + +@Controller('notifications') +export class ClientNotificationController { + constructor( + private botTelegramApi: BotTelegramApi, + private readonly notifyService: NotificationService, + private readonly bidsService: BidsService, + ) {} + + @Post('send-messages') + async sendMessage(@Body() data: SendMessageDto) { + return await this.botTelegramApi.sendMessage(data.text, data.options); + } + + @Post('test') + async test() { + const bid = await this.bidsService.bidsRepo.findOne({ + where: { lot_id: '26077023' }, + }); + + return await this.notifyService.emitBidStatus({ + ...bid, + status: 'win-bid', + }); + } +} diff --git a/auto-bid-server/src/modules/notification/controllers/notification.controller.ts b/auto-bid-server/src/modules/notification/controllers/notification.controller.ts new file mode 100644 index 0000000..4deec46 --- /dev/null +++ b/auto-bid-server/src/modules/notification/controllers/notification.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { BotTelegramApi } from '../../bids/apis/bot-telegram.api'; + +@Controller('admin/notifications') +export class NotificationController { + constructor(private botTelegramApi: BotTelegramApi) {} + + @Get('') + async test() { + return await this.botTelegramApi.getGroupInfo(); + } +} diff --git a/auto-bid-server/src/modules/notification/dto/send-message.dto.ts b/auto-bid-server/src/modules/notification/dto/send-message.dto.ts new file mode 100644 index 0000000..fc3a5fa --- /dev/null +++ b/auto-bid-server/src/modules/notification/dto/send-message.dto.ts @@ -0,0 +1,9 @@ +import { IsObject, IsString } from 'class-validator'; + +export class SendMessageDto { + @IsString() + text: string; + + @IsObject() + options: Record; +} diff --git a/auto-bid-server/src/modules/notification/notification.controller.spec.ts b/auto-bid-server/src/modules/notification/notification.controller.spec.ts index 04dfa50..26b989b 100644 --- a/auto-bid-server/src/modules/notification/notification.controller.spec.ts +++ b/auto-bid-server/src/modules/notification/notification.controller.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { NotificationController } from './notification.controller'; +import { NotificationController } from './controllers/notification.controller'; import { NotificationService } from './notification.service'; describe('NotificationController', () => { diff --git a/auto-bid-server/src/modules/notification/notification.controller.ts b/auto-bid-server/src/modules/notification/notification.controller.ts deleted file mode 100644 index e953096..0000000 --- a/auto-bid-server/src/modules/notification/notification.controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { BotTelegramApi } from '../bids/apis/bot-telegram.api'; -import { NotificationService } from './notification.service'; - -@Controller('admin/notifications') -export class NotificationController { - constructor( - private readonly notificationService: NotificationService, - private botTelegramApi: BotTelegramApi, - ) {} - - @Get('') - async test() { - return await this.botTelegramApi.getGroupInfo(); - } -} diff --git a/auto-bid-server/src/modules/notification/notification.module.ts b/auto-bid-server/src/modules/notification/notification.module.ts index ddbbd19..b01ffe3 100644 --- a/auto-bid-server/src/modules/notification/notification.module.ts +++ b/auto-bid-server/src/modules/notification/notification.module.ts @@ -1,17 +1,20 @@ import { forwardRef, Module } from '@nestjs/common'; -import { NotificationService } from './notification.service'; -import { NotificationController } from './notification.controller'; -import { BidsModule } from '../bids/bids.module'; -import { Notification } from './entities/notification.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { BidsModule } from '../bids/bids.module'; +import { ClientNotificationController } from './controllers/client-notification.controller'; +import { NotificationController } from './controllers/notification.controller'; +import { Notification } from './entities/notification.entity'; import { AdminNotificationListener } from './listeners/admin-notification.listener'; +import { NotificationService } from './notification.service'; +import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service'; +import { SendMessageHistory } from '../bids/entities/send-message-histories.entity'; @Module({ imports: [ forwardRef(() => BidsModule), - TypeOrmModule.forFeature([Notification]), + TypeOrmModule.forFeature([Notification, SendMessageHistory]), ], - controllers: [NotificationController], + controllers: [NotificationController, ClientNotificationController], providers: [NotificationService, AdminNotificationListener], exports: [NotificationService], }) diff --git a/auto-bid-server/src/modules/notification/notification.service.ts b/auto-bid-server/src/modules/notification/notification.service.ts index 4fcaf71..7f66f02 100644 --- a/auto-bid-server/src/modules/notification/notification.service.ts +++ b/auto-bid-server/src/modules/notification/notification.service.ts @@ -5,7 +5,7 @@ import { NAME_EVENTS } from './constants'; import { BotTelegramApi } from '../bids/apis/bot-telegram.api'; import { InjectRepository } from '@nestjs/typeorm'; import { Notification } from './entities/notification.entity'; -import { Repository } from 'typeorm'; +import { LEGAL_TCP_SOCKET_OPTIONS, Repository } from 'typeorm'; import { isTimeReached } from '@/ultils'; import { FilterOperator, @@ -15,6 +15,8 @@ import { } from 'nestjs-paginate'; import { Column } from 'nestjs-paginate/lib/helper'; import AppResponse from '@/response/app-response'; +import { SendMessageHistoriesService } from '../bids/services/send-message-histories.service'; +import { SendMessageHistory } from '../bids/entities/send-message-histories.entity'; @Injectable() export class NotificationService { @@ -23,6 +25,8 @@ export class NotificationService { private readonly botTelegramApi: BotTelegramApi, @InjectRepository(Notification) readonly notificationRepo: Repository, + @InjectRepository(SendMessageHistory) + readonly sendMessageRepo: Repository, ) {} async index(query: PaginateQuery) { @@ -50,6 +54,8 @@ export class NotificationService { } getBidStatusMessage(bid: Bid): string | null { + const itemName = `*${bid.name || 'the item'}*`; + switch (bid.status) { case 'biding': return !bid.name ? null : `✅ The item has been activated. ${bid.name}`; @@ -59,8 +65,6 @@ export class NotificationService { return `⏳ The auction for *${bid.name || 'this item'}* has ended.`; } - const itemName = `*${bid.name || 'the item'}*`; - if ( bid.max_price + bid.plus_price <= bid.current_price || bid.reserve_price > bid.max_price + bid.plus_price @@ -85,6 +89,14 @@ export class NotificationService { const message = this.getBidStatusMessage(bid); if (!message) return; + if (bid.status === 'win-bid') { + const prevSendMessage = await this.sendMessageRepo.findOne({ + where: { bid: { id: bid.id }, type: 'win-bid' }, + }); + + if (prevSendMessage) return; + } + const notification = await this.notificationRepo.save({ message, raw_data: JSON.stringify({ @@ -97,6 +109,12 @@ export class NotificationService { send_to: JSON.stringify(sendToData), }); + await this.sendMessageRepo.save({ + bid: { id: bid.id }, + message: notification.message, + type: bid.status, + }); + this.eventEmitter.emit(NAME_EVENTS.BID_STATUS, { bid: { ...bid, diff --git a/auto-bid-server/src/ultils/index.ts b/auto-bid-server/src/ultils/index.ts index e68e59c..c7d2231 100644 --- a/auto-bid-server/src/ultils/index.ts +++ b/auto-bid-server/src/ultils/index.ts @@ -1,6 +1,18 @@ export function extractModelId(url: string): string | null { - const match = url.match(/\/lot\/([\d-]+)\//); - return match ? match[1] : null; + switch (extractDomain(url)) { + case 'https://www.grays.com': { + const match = url.match(/\/lot\/([\d-]+)\//); + return match ? match[1] : null; + } + case 'https://www.langtons.com.au': { + const match = url.match(/auc-var-\d+/); + return match[0]; + } + case 'https://www.lawsons.com.au': { + const match = url.split('_'); + return match ? match[1] : null; + } + } } export function subtractMinutes(timeStr: string, minutes: number) { @@ -28,3 +40,60 @@ export function extractDomain(url: string): string | null { export function escapeMarkdownV2(text: string) { return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&'); } + +export function extractVerifyCodeLANGTONS(content: string) { + const match = content.match(/Your\s+(\w+)\s+verific.*?code\s+is:\s+(\d+)/i); + + if (match) { + const name = match[1]; // LANGTONS + const code = match[2]; // 012152 + + return { + name, + code, + }; + } else { + return null; + } +} + +export function extractVerifyCodeLAWSONS(content: string) { + if (!content) return null; + + // Loại bỏ các ký tự `=` không cần thiết do email encoding (quoted-printable) + content = content.replace(/=\r\n/g, '').replace(/=3D/g, '='); + + // Regex để tìm mã xác minh (6 chữ số) + const codeMatch = content.match(/Verification code:\s*(\d{6})/); + const code = codeMatch ? codeMatch[1] : null; + + // Regex để tìm tên (trong đoạn "Lawsons | Sydney, AU") + const nameMatch = content.match(/([\w\s]+)\s*\|\s*[\w\s,]+/); + const name = nameMatch ? nameMatch[1].trim() : null; + + // Nếu không tìm thấy cả hai giá trị, trả về null + if (!code && !name) { + return null; + } + + return { code, name }; +} + +export function verifyCode(content: string) { + // Kiểm tra mã xác minh từ LANGTONS + const langtonsResult = extractVerifyCodeLANGTONS(content); + + if (langtonsResult) { + return langtonsResult; + } + + // Nếu không có kết quả, tiếp tục kiểm tra mã xác minh từ LAWSONS + const lawsonsResult = extractVerifyCodeLAWSONS(content); + + if (lawsonsResult) { + return lawsonsResult; // Trả về kết quả đúng thay vì langtonsResult + } + + // Nếu không tìm thấy mã xác minh, trả về null + return null; +} diff --git a/auto-bid-tool/.vscode/settings.json b/auto-bid-tool/.vscode/settings.json new file mode 100644 index 0000000..93e41f2 --- /dev/null +++ b/auto-bid-tool/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["Langtons", "Lawsons"] +} diff --git a/auto-bid-tool/data/fake-out-lot-langtons.json b/auto-bid-tool/data/fake-out-lot-langtons.json new file mode 100644 index 0000000..e7eba3c --- /dev/null +++ b/auto-bid-tool/data/fake-out-lot-langtons.json @@ -0,0 +1,29 @@ +{ + "action": "Auction-LotsData", + "queryString": "auctionBpa=18&auctionCatalogPrice=16&auctionId=7402&auctionUnitQuantity=1&includeQuickViewLotData=true&lotIds=26033882&pid=auc-var-26033882&productName=VICKERY%20WINES%20Riesling%2C%20Eden%20Valley%202016%20Bottle", + "locale": "en_AU", + "csrf": { + "tokenName": "csrf_token", + "token": "I0GEkb97Y1mbVWixUo2f7sXj4EChyVDMO-CT3I6vV90VEfRJFLio5eBbartpUOaiJUZci22TbjoAnR-UVxaIJ2Umj51lWikd3oUxsTGM_ObrtgFiQXQqxJjY4D98BWf7_tI4uAu-kG6uttjdwArabsiJMx6qDGG0wvH2FLBQ8fpK9IjIBXo=" + }, + "success": true, + "lotData": { + "lotId": "26033882", + "currentMaxBid": 17, + "minimumBid": 18, + "numberOfBidders": 2, + "numberOfWatchers": 1, + "hasMetReserve": true, + "bidStatus": "Winning", + "timestamp": 49568786, + "myBid": "20" + }, + "productBPA": "18.00", + "priceIncBPA": "$18.88", + "auctionPrice": "$16.00", + "totalAuctionPrice": "$16.00", + "lotId": "26033882", + "pid": "auc-var-26033882", + "unitQuantity": "1", + "auctionBpa": "18" +} diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js index da48190..8d2cfee 100644 --- a/auto-bid-tool/index.js +++ b/auto-bid-tool/index.js @@ -7,6 +7,8 @@ import browser from './system/browser.js'; import configs from './system/config.js'; import { delay, isTimeReached, safeClosePage } from './system/utils.js'; +global.IS_CLEANING = true; + let MANAGER_BIDS = []; const activeTasks = new Set(); @@ -129,6 +131,11 @@ const tracking = async () => { }; const clearLazyTab = async () => { + if (!global.IS_CLEANING) { + console.log('🚀 Cleaning flag is OFF. Proceeding with operation.'); + return; + } + if (!browser) { console.warn('⚠️ Browser is not available or disconnected.'); return; @@ -203,6 +210,9 @@ const workTracking = async () => { }, }); + // set socket on global app + global.socket = socket; + // listen connect socket.on('connect', () => { console.log('✅ Connected to WebSocket server'); @@ -229,11 +239,16 @@ const workTracking = async () => { if (isDeleted) { console.log('✅ Profile deleted successfully!'); - const tabs = MANAGER_BIDS.filter((item) => item.url === data.url || item?.web_bid.url === data.url); + const tab = MANAGER_BIDS.find((item) => item.url === data.url); - if (tabs.length <= 0) return; + if (!tab) return; - await Promise.all(tabs.map((tab) => safeClosePage(tab))); + global.IS_CLEANING = false; + await Promise.all(tab.children.map((tab) => safeClosePage(tab))); + + await safeClosePage(tab); + + global.IS_CLEANING = true; } else { console.log('⚠️ No profile found to delete.'); } 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 3da2809..b6fcce5 100644 --- a/auto-bid-tool/models/grays.com/grays-api-bid.js +++ b/auto-bid-tool/models/grays.com/grays-api-bid.js @@ -39,44 +39,43 @@ export class GrayApiBid extends ApiBid { // } // }); - console.log('🔄 Starting polling process...'); + console.log(`🔄 [${this.id}] Starting polling process...`); await page.evaluateHandle( - (apiUrl, interval) => { + (apiUrl, interval, bidId) => { if (window._autoBidPollingStarted) { - console.log('✅ Polling is already running. Skipping initialization.'); + console.log(`✅ [${bidId}] Polling is already running. Skipping initialization.`); return; } - console.log('🚀 Initializing polling...'); + console.log(`🚀 [${bidId}] Initializing polling...`); window._autoBidPollingStarted = true; function sendRequest() { - console.log('📡 Sending request to track out-bid lots...'); + 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(`✅ Response received: ${response.status}`)) - .catch((err) => console.error('⚠️ Request error:', err)); + .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, ); - - console.log('✅ Polling successfully started!'); } catch (error) { if (error.message.includes('Execution context was destroyed')) { - console.log('⚠️ Page reload detected, restarting polling...'); + console.log(`⚠️ [${this.id}] Page reload detected, restarting polling...`); await page.waitForNavigation({ waitUntil: 'networkidle2' }).catch(() => {}); return await this.polling(page); } - console.error('🚨 Unexpected polling error:', error); + console.error(`🚨 [${this.id}] Unexpected polling error:`, error); throw error; } } @@ -110,7 +109,7 @@ export class GrayApiBid extends ApiBid { for (const product_tab of handleChildren) { if (!isTimeReached(product_tab.start_bid_time)) { - console.log("❌ It's not time yet ID: " + product_tab.id + ' continue waiting...'); + console.log(`❌ [${this.id}] It's not time yet ID: ${product_tab.id} continue waiting...`); return; } @@ -129,17 +128,20 @@ export class GrayApiBid extends ApiBid { 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('✅ Already logged in, skipping login.'); + console.log(`✅ [${this.id}] Already logged in, skipping login.`); + global.IS_CLEANING = true; this.retry_login = 0; // Reset retry count return; } - console.log('🔑 Starting login process...'); + console.log(`🔑 [${this.id}] Starting login process...`); try { await page.type('input[name="username"]', this.username, { delay: 100 }); @@ -152,18 +154,18 @@ export class GrayApiBid extends ApiBid { ]); if (!(await page.$('input[name="username"]'))) { - console.log('✅ Login successful!'); + 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(`⚠️ Login error: ${error.message}. Retrying attempt ${this.retry_login + 1} ❌`); + 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('🚨 Maximum login attempts reached. Stopping login process.'); + console.log(`🚨 [${this.id}] Maximum login attempts reached. Stopping login process.`); safeClosePage(this); this.retry_login = 0; // Reset retry count return; @@ -177,6 +179,8 @@ export class GrayApiBid extends ApiBid { } return await this.action(); // Retry login + } finally { + global.IS_CLEANING = true; } } @@ -185,43 +189,38 @@ export class GrayApiBid extends ApiBid { 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'); - - // await takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK); + console.log(`🛠️ [${this.id}] UserAgent set.`); page.on('response', async (response) => { if (response.request().url().includes('api/Notifications/GetOutBidLots')) { - console.log('🚀 API POST:', response.url()); + 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('❌ Error get response', error?.message); + console.error(`❌ [${this.id}] Error processing response:`, error?.message); } } }); page.on('load', async () => { - console.log('🔄 The page has reloaded, restarting polling...'); - - // await takeSnapshot(this.page_context, this, 'working', CONSTANTS.TYPE_IMAGE.WORK); - + console.log(`🔄 [${this.id}] Page has reloaded, restarting polling...`); await this.polling(page); - await this.handleLogin(); }); - await this.polling(page); // Call when fist load - + await this.polling(page); // Call when first load await this.handleLogin(); } catch (error) { - console.log(error.message); + 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 823f3e5..7a38b64 100644 --- a/auto-bid-tool/models/grays.com/grays-product-bid.js +++ b/auto-bid-tool/models/grays.com/grays-product-bid.js @@ -10,12 +10,12 @@ export class GraysProductBid extends ProductBid { async validate({ page, price_value }) { if (!this.start_bid_time || !isTimeReached(this.start_bid_time)) { - console.log("It's not time yet ❌"); + console.log(`❌ [${this.id}] It's not time yet`); return { result: false, bid_price: 0 }; } if (!isNumber(price_value)) { - console.log("Can't get PRICE_VALUE ❌"); + console.log(`❌ [${this.id}] Can't get PRICE_VALUE`); await takeSnapshot(page, this, 'price-value-null'); return { result: false, bid_price: 0 }; @@ -24,7 +24,7 @@ export class GraysProductBid extends ProductBid { const bid_price = this.plus_price + Number(price_value); if (bid_price > this.max_price) { - console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌'); + 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); @@ -92,7 +92,7 @@ export class GraysProductBid extends ProductBid { await delay(500); if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) { - console.log(`Product is close ${close_time} ❌`); + console.log(`❌ [${this.id}] Product is close ${close_time}`); return { result: true, close_time }; } @@ -123,7 +123,7 @@ export class GraysProductBid extends ProductBid { await takeSnapshot(page, this, 'bid-success', CONSTANTS.TYPE_IMAGE.SUCCESS); return true; } catch (error) { - console.log('❌ Timeout to loading'); + console.log(`❌ [${this.id}] Timeout to loading`); await takeSnapshot(page, this, 'timeout to loading'); return false; } @@ -169,7 +169,7 @@ export class GraysProductBid extends ProductBid { .$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText) .catch(() => null); - console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`); + console.log(`📌 [${this.id}] Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`); const data = removeFalsyValues( { @@ -196,54 +196,50 @@ export class GraysProductBid extends ProductBid { 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('❌ The product is closed, cannot place a bid.'); + 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('❌ Validation failed. Unable to proceed with bidding.'); + 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(`🔄 You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`); + console.log(`🔄 [${this.id}] You have already bid on this item! (Bid Price: ${bidHistoriesItem.price})`); return; } if (price_value != bid_price) { - console.log(`✍️ Updating bid price from ${price_value} → ${bid_price}`); + console.log(`✍️ [${this.id}] Updating bid price from ${price_value} → ${bid_price}`); await this.handleWritePrice(page, bid_price); } - console.log('🚀 Placing the bid...'); + console.log(`🚀 [${this.id}] Placing the bid...`); const resultPlaceBid = await this.placeBid(page); - if (!resultPlaceBid) { - console.log('❌ Error occurred while placing the bid.'); + console.log(`❌ [${this.id}] Error occurred while placing the bid.`); await takeSnapshot(page, this, 'place-bid-action'); return; } - console.log(`✅ Bid placed successfully! 🏆 Bid Price: ${bid_price}, Closing Time: ${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(`🚨 Error navigating the page: ${error.message}`); + 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 new file mode 100644 index 0000000..c1202db --- /dev/null +++ b/auto-bid-tool/models/langtons.com.au/langtons-api-bid.js @@ -0,0 +1,225 @@ +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); + } + + waitVerifyData = async () => + new Promise((rev, rej) => { + // Tạo timeout để reject sau 1 phút nếu không có phản hồi + const timeout = setTimeout(() => { + global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ + rej(new Error(`[${this.id}] Timeout: No verification code received within 1 minute.`)); + }, 60 * 1000); // 60 giây + + global.socket.on(`verify-code.${this.origin_url}`, async (data) => { + console.log(`📢 [${this.id}] VERIFY CODE:`, data); + clearTimeout(timeout); // Hủy timeout vì đã nhận được mã + global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại + rev(data); // Resolve với dữ liệu nhận được + }); + }); + + async isLogin() { + 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; + } + + 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: 8000, waitUntil: 'domcontentloaded' }); + + // 📂 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; + } + + // 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); + + // 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))); + + // 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(); + } + } + }); + + await page.goto(this.url, { waitUntil: 'networkidle2' }); + + await page.bringToFront(); + + // Set userAgent + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + } catch (error) { + console.log('Error [action]: ', error.message); + } + }; + + listen_events = async () => { + if (this.page_context) return; + + await this.puppeteer_connect(); + await this.action(); + + this.reloadInterval = setInterval(async () => { + try { + if (this.page_context && !this.page_context.isClosed()) { + console.log(`🔄 [${this.id}] Reloading page...`); + await this.page_context.reload({ waitUntil: 'networkidle2' }); + console.log(`✅ [${this.id}] Page reloaded successfully.`); + + 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); + }; +} 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 new file mode 100644 index 0000000..f22cc25 --- /dev/null +++ b/auto-bid-tool/models/langtons.com.au/langtons-product-bid.js @@ -0,0 +1,394 @@ +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, removeFalsyValues, takeSnapshot } from '../../system/utils.js'; +import { ProductBid } from '../product-bid.js'; + +export class LangtonsProductBid extends ProductBid { + 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() + 2 * 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 = 10000) { + 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(() => { + console.log(`⏳ [${this.id}] Timeout: No response received within 10s`); + this.page_context.off('response', onResponse); // Gỡ bỏ listener khi timeout + resolve(null); + }, timeout); + + this.page_context.on('response', onResponse); + }); + } + + async getName() { + try { + if (!this.page_context) return null; + + await this.page_context.waitForSelector('.product-name', { timeout: 3000 }); + + return await this.page_context.evaluate(() => { + const el = document.querySelector('.product-name'); + return el ? el.innerText : null; + }); + } catch (error) { + return null; + } + } + + async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price, model }) { + const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0, model }); + + if (response) { + this.lot_id = response.lot_id; + this.close_time = response.close_time; + this.start_bid_time = response.start_bid_time; + } + } + + update = async () => { + if (!this.page_context) return; + + console.log(`🔄 [${this.id}] Call update for ID: ${this.id}`); + + // 📌 Lấy thời gian kết thúc đấu giá từ giao diện + const close_time = await this.getCloseTime(); + console.log(`⏳ [${this.id}] Retrieved close time: ${close_time}`); + + // 📌 Lấy tên sản phẩm hoặc thông tin liên quan + const name = await this.getName(); + console.log(`📌 [${this.id}] Retrieved name: ${name}`); + + // 📌 Chờ phản hồi API từ trang, tối đa 10 giây + const result = await this.waitForApiResponse(); + + // 📌 Nếu không có dữ liệu trả về thì dừng + if (!result) { + console.log(`⚠️ [${this.id}] No valid data received, skipping update.`); + return; + } + + // 📌 Loại bỏ các giá trị không hợp lệ và bổ sung thông tin cần thiết + const data = removeFalsyValues( + { + model: result?.pid || null, + lot_id: result?.lotId || null, + reserve_price: result.lotData?.minimumBid || null, + current_price: result.lotData?.currentMaxBid || null, + // close_time: close_time && !this.close_time ? String(close_time) : null, + close_time: close_time ? String(close_time) : null, + name, + }, + ['close_time'], + ); + + console.log(`🚀 [${this.id}] Processed data ready for update`); + + // 📌 Gửi dữ liệu cập nhật lên hệ thống + await this.handleUpdateBid(data); + + console.log('✅ Update successful!'); + + return { ...response, name, close_time }; + }; + + async getContinueShopButton() { + try { + if (!this.page_context) return null; + + await this.page_context.waitForSelector('.btn.btn-block.btn-primary.error.continue-shopping', { timeout: 3000 }); + + return await this.page_context.evaluate(() => { + const el = document.querySelector('.btn.btn-block.btn-primary.error.continue-shopping'); + + return el; + }); + } catch (error) { + return null; + } + } + + async handlePlaceBid() { + if (!this.page_context) { + console.log(`⚠️ [${this.id}] No page context found, aborting bid process.`); + return; + } + const page = this.page_context; + + 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...`); + 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 + } + + // Đợ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: 100 }); + 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: 10000 }); + 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}`); + 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 != 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-api-bid.js b/auto-bid-tool/models/lawsons.com.au/lawsons-api-bid.js new file mode 100644 index 0000000..9c491dd --- /dev/null +++ b/auto-bid-tool/models/lawsons.com.au/lawsons-api-bid.js @@ -0,0 +1,260 @@ +import fs from 'fs'; +import configs from '../../system/config.js'; +import { delay, getPathProfile, safeClosePage } from '../../system/utils.js'; +import { ApiBid } from '../api-bid.js'; + +export class LawsonsApiBid extends ApiBid { + reloadInterval = null; + constructor({ ...prev }) { + super(prev); + } + + waitVerifyData = async () => + new Promise((rev, rej) => { + // Tạo timeout để reject sau 1 phút nếu không có phản hồi + const timeout = setTimeout(() => { + global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ + rej(new Error(`[${this.id}] Timeout: No verification code received within 1 minute.`)); + }, 60 * 1000); // 60 giây + + global.socket.on(`verify-code.${this.origin_url}`, async (data) => { + console.log(`📢 [${this.id}] VERIFY CODE:`, data); + clearTimeout(timeout); // Hủy timeout vì đã nhận được mã + global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại + rev(data); // Resolve với dữ liệu nhận được + }); + }); + + async isLogin() { + if (!this.page_context) return false; + + const filePath = getPathProfile(this.origin_url); + + return !(await this.page_context.$('#emailLogin')) && fs.existsSync(filePath); + } + + waitVerifyData = async () => + new Promise((rev, rej) => { + // Tạo timeout để reject sau 1 phút nếu không có phản hồi + const timeout = setTimeout(() => { + global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh rò rỉ bộ nhớ + rej(new Error(`[${this.id}] Timeout: No verification code received within 1 minute.`)); + }, 60 * 1000); // 60 giây + + global.socket.on(`verify-code.${this.origin_url}`, async (data) => { + console.log(`📢 [${this.id}] VERIFY CODE:`, data); + clearTimeout(timeout); // Hủy timeout vì đã nhận được mã + global.socket.off(`verify-code.${this.origin_url}`); // Xóa listener tránh lặp lại + rev(data); // Resolve với dữ liệu nhận được + }); + }); + + async enterOTP(otp) { + try { + // Selector cho tất cả các input OTP + const inputSelector = '.MuiDialog-container .container input'; + + // Chờ cho các input OTP xuất hiện + await this.page_context.waitForSelector(inputSelector, { timeout: 8000 }); + + // Lấy tất cả các input OTP + const inputs = await this.page_context.$$(inputSelector); + + // Kiểm tra nếu có đúng 6 trường input + if (inputs.length === 6 && otp.length === 6) { + // Nhập mỗi ký tự của OTP vào các input tương ứng + for (let i = 0; i < 6; i++) { + await inputs[i].type(otp[i], { delay: 100 }); + } + console.log(`✅ OTP entered successfully: ${otp}`); + } else { + console.error('❌ Invalid OTP or input fields count'); + } + } catch (error) { + console.error('❌ Error entering OTP:', error); + } + } + + async waitToTwoVerify() { + try { + if (!this.page_context) return false; + + // Selector của các phần tử trên trang + const button = '.form-input-wrapper.form-group > .btn.btn-primary'; // Nút để tiếp tục quá trình xác minh + const remember = '.PrivateSwitchBase-input'; // Checkbox "Remember me" + const continueButton = + '.MuiButtonBase-root.MuiButton-root.MuiButton-contained.MuiButton-containedPrimary.MuiButton-sizeMedium.MuiButton-containedSizeMedium.MuiButton-colorPrimary.MuiButton-root'; // Nút "Continue" + + // Chờ cho nút xác minh xuất hiện + console.log(`🔎 [${this.id}] Waiting for the button with selector: ${button}`); + await this.page_context.waitForSelector(button, { timeout: 8000 }); + + console.log(`✅ [${this.id}] Button found, clicking the first button.`); + + // Lấy phần tử của nút và log nội dung của nó + const firstButton = await this.page_context.$(button); // Lấy phần tử đầu tiên + const buttonContent = await firstButton.evaluate((el) => el.textContent); // Lấy nội dung của nút + console.log(`🔎 [${this.id}] Button content: ${buttonContent}`); + + // Chờ 2s cho button sẵn sàn + await delay(2000); + // Click vào nút xác minh + await firstButton.click(); + console.log(`✅ [${this.id}] Button clicked.`); + + // Nhận mã OTP để nhập vào form + const { name, code } = await this.waitVerifyData(); + console.log(`🔎 [${this.id}] Waiting for OTP input, received code: ${code}`); + + // Nhập mã OTP vào form + await this.enterOTP(code); + console.log(`✅ [${this.id}] OTP entered successfully.`); + + // Chờ cho checkbox "Remember me" xuất hiện + await this.page_context.waitForSelector(remember, { timeout: 8000 }); + console.log(`🔎 [${this.id}] Waiting for remember me checkbox with selector: ${remember}`); + + // Click vào checkbox "Remember me" + await this.page_context.click(remember, { delay: 92 }); + console.log(`✅ [${this.id}] Remember me checkbox clicked.`); + + // Chờ cho nút "Continue" xuất hiện + await this.page_context.waitForSelector(continueButton, { timeout: 8000 }); + console.log(`🔎 [${this.id}] Waiting for continue button with selector: ${continueButton}`); + + // Click vào nút "Continue" + await this.page_context.click(continueButton, { delay: 100 }); + console.log(`✅ [${this.id}] Continue button clicked.`); + + // Chờ cho trang tải hoàn tất sau khi click "Continue" + await this.page_context.waitForNavigation({ waitUntil: 'domcontentloaded' }); + console.log(`✅ [${this.id}] Navigation completed.`); + + return true; + } catch (error) { + console.error(`❌ [${this.id}] Error:`, error); + return false; + } + } + + async handleLogin() { + const page = this.page_context; + + global.IS_CLEANING = false; + + const filePath = getPathProfile(this.origin_url); + + await page.waitForNavigation({ waitUntil: 'domcontentloaded' }); + + // 🛠 Check if already logged in (login input should not be visible or profile exists) + if (!(await page.$('#emailLogin')) && fs.existsSync(filePath)) { + console.log(`✅ [${this.id}] Already logged in, skipping login process.`); + return; + } + + if (fs.existsSync(filePath)) { + console.log(`🗑 [${this.id}] Deleting existing file: ${filePath}`); + fs.unlinkSync(filePath); + } + + const children = this.children.filter((item) => item.page_context); + console.log(`🔍 [${this.id}] Found ${children.length} child pages to close.`); + + if (children.length > 0) { + console.log(`🛑 [${this.id}] Closing child pages...`); + await Promise.all( + children.map((item) => { + console.log(`➡ [${this.id}] Closing child page with context: ${item.page_context}`); + return safeClosePage(item); + }), + ); + + console.log(`➡ [${this.id}] Closing main page context: ${this.page_context}`); + await safeClosePage(this); + } + + console.log(`🔑 [${this.id}] Starting login process...`); + + try { + // ⌨ Enter email + console.log(`✍ [${this.id}] Entering email:`, this.username); + await page.type('#emailLogin', this.username, { delay: 100 }); + + // ⌨ Enter password + console.log(`✍ [${this.id}] Entering password...`); + await page.type('#passwordLogin', this.password, { delay: 150 }); + + // 🚀 Click the login button + console.log(`🔘 [${this.id}] Clicking the "Login" button`); + await page.click('#signInBtn', { delay: 92 }); + + const result = await this.waitToTwoVerify(); + + // ⏳ Wait for navigation after login + if (!result) { + console.log(`⏳ [${this.id}] Waiting for navigation after login...`); + await page.waitForNavigation({ timeout: 8000, waitUntil: 'domcontentloaded' }); + } + + if (this.page_context.url() == this.url) { + // 📂 Save session context to avoid re-login + await this.saveContext(); + console.log(`✅ [${this.id}] Login successful!`); + } else { + console.log(`❌ [${this.id}] Login Failure!`); + } + } catch (error) { + console.error(`❌ [${this.id}] Error during login process:`, error.message); + } finally { + global.IS_CLEANING = true; + } + } + + action = async () => { + try { + const page = this.page_context; + + page.on('response', async (response) => { + const request = response.request(); + if (request.redirectChain().length > 0) { + if (response.url().includes(configs.WEB_CONFIGS.LAWSONS.LOGIN_URL)) { + await this.handleLogin(); + } + } + }); + + await page.goto(this.url, { waitUntil: 'networkidle2' }); + + await page.bringToFront(); + + // Set userAgent + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + } catch (error) { + console.log('Error [action]: ', error.message); + } + }; + + listen_events = async () => { + if (this.page_context) return; + + await this.puppeteer_connect(); + await this.action(); + + this.reloadInterval = setInterval(async () => { + try { + if (this.page_context && !this.page_context.isClosed()) { + console.log(`🔄 [${this.id}] Reloading page...`); + await this.page_context.reload({ waitUntil: 'networkidle2' }); + console.log(`✅ [${this.id}] Page reloaded successfully.`); + + // 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); + }; +} 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 new file mode 100644 index 0000000..9984440 --- /dev/null +++ b/auto-bid-tool/models/lawsons.com.au/lawsons-product-bid.js @@ -0,0 +1,114 @@ +import { updateBid } from '../../system/apis/bid.js'; +import configs from '../../system/config.js'; +import { extractPriceNumber, removeFalsyValues } from '../../system/utils.js'; +import { ProductBid } from '../product-bid.js'; + +export class LawsonsProductBid 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; + } + } + + async getReversePrice() { + 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('.select-dropdown-value.text-truncate', { timeout: 2000 }); + 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); + // Nếu có lỗi xảy ra trong quá trình lấy thời gian, trả về null + return null; + } + } + + update = async () => { + try { + if (!this.page_context) return; + + 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, + 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); + }; + + 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 }; + } + + 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(); + } + } 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 62c5dee..8f845fe 100644 --- a/auto-bid-tool/models/product-bid.js +++ b/auto-bid-tool/models/product-bid.js @@ -135,16 +135,6 @@ export class ProductBid extends Bid { return true; } - // async gotoLink() { - // const page = this.page_context; - // console.log('🔄 Starting the bidding process...'); - - // await page.goto(this.url, { waitUntil: 'networkidle2' }); - // console.log(`✅ Navigated to: ${this.url}`); - - // await page.bringToFront(); - // console.log('👀 Brought the tab to the foreground.'); - // } async gotoLink() { const page = this.page_context; @@ -160,6 +150,7 @@ export class ProductBid extends Bid { 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); diff --git a/auto-bid-tool/service/app-service.js b/auto-bid-tool/service/app-service.js index f01c4b8..747f004 100644 --- a/auto-bid-tool/service/app-service.js +++ b/auto-bid-tool/service/app-service.js @@ -1,13 +1,16 @@ +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 * as fs from 'fs'; -import _ from 'lodash'; +import { LawsonsApiBid } from '../models/lawsons.com.au/lawsons-api-bid.js'; +import { LawsonsProductBid } from '../models/lawsons.com.au/lawsons-product-bid.js'; -const ONE_MINUTE = 60 * 1000; +const TIME = 30 * 1000; export const handleCloseRemoveProduct = (data) => { if (!Array.isArray(data)) return; @@ -24,6 +27,12 @@ export const createBidProduct = (web, data) => { 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 }); + } } }; @@ -32,6 +41,12 @@ export const createApiBid = (web) => { 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 }); + } } }; @@ -49,5 +64,6 @@ export const deleteProfile = (data) => { export const shouldUpdateProductTab = (productTab) => { const updatedAt = new Date(productTab.updated_at).getTime(); const now = Date.now(); - return now - updatedAt >= ONE_MINUTE; + + return now - updatedAt >= TIME; }; diff --git a/auto-bid-tool/system/apis/bid.js b/auto-bid-tool/system/apis/bid.js index bae85f1..cf829ce 100644 --- a/auto-bid-tool/system/apis/bid.js +++ b/auto-bid-tool/system/apis/bid.js @@ -14,7 +14,7 @@ export const getBids = async () => { return data.data; } catch (error) { - console.log('❌ ERROR IN SERVER: ', error); + console.log('❌ ERROR IN SERVER (GET BIDS): ', error); return []; } }; @@ -34,7 +34,7 @@ export const updateBid = async (id, values) => { return data.data; } catch (error) { - console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error.response); + console.log('❌ ERROR IN SERVER: (UPDATE BID) ', error); return null; } }; @@ -73,7 +73,7 @@ export const pushPrice = async (values) => { return { status: true, data: data.data }; } catch (error) { - console.log('❌ ERROR IN SERVER: ', error); + console.log('❌ ERROR IN SERVER (PUSH PRICE): ', error.response.data); return { status: false, data: [] }; } }; @@ -95,7 +95,10 @@ export const updateStatusByPrice = async (id, current_price) => { return { status: true, data: data.data }; } catch (error) { - console.log('❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ', error.message); + console.log('❌ ERROR IN SERVER:(UPDATE STATUS BY PRICE) ', { + // response: error.response, + message: error.message, + }); return { status: false, data: [] }; } }; diff --git a/auto-bid-tool/system/apis/notification.js b/auto-bid-tool/system/apis/notification.js new file mode 100644 index 0000000..cbf20a5 --- /dev/null +++ b/auto-bid-tool/system/apis/notification.js @@ -0,0 +1,21 @@ +import axios from '../axios.js'; + +export const sendMessage = async (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; + } +}; diff --git a/auto-bid-tool/system/config.js b/auto-bid-tool/system/config.js index 932a8d0..1c2ba52 100644 --- a/auto-bid-tool/system/config.js +++ b/auto-bid-tool/system/config.js @@ -4,12 +4,29 @@ const configs = { 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`, }, 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}`; + }, + }, }, }; diff --git a/auto-bid-tool/system/utils.js b/auto-bid-tool/system/utils.js index 502940c..c1f0118 100644 --- a/auto-bid-tool/system/utils.js +++ b/auto-bid-tool/system/utils.js @@ -9,7 +9,6 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_ if (!page || page.isClosed()) return; try { - global.IS_CLEANING = false; 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 @@ -101,3 +100,81 @@ export function removeFalsyValues(obj, excludeKeys = []) { return acc; }, {}); } + +export const enableAutoBidMessage = (data) => { + return ` + ⭉ Activate Auto Bid
+ 📌 Product: ${data.name}
+ 🔗 Link: Click here
+ 💰 Max Price: $${data.max_price}
+ 🌐 Platform: Langtons + `; +}; + +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, + }; + + // 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; + + // 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)); + + // 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 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; + } + + // 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); + + // Trả về chuỗi UTC + return utcDate.toUTCString(); +} + +export function extractPriceNumber(priceString) { + const cleaned = priceString.replace(/[^\d.]/g, ''); + return parseFloat(cleaned); +}