diff --git a/auto-bid-admin/src/apis/send-message-histories.ts b/auto-bid-admin/src/apis/send-message-histories.ts new file mode 100644 index 0000000..b397b15 --- /dev/null +++ b/auto-bid-admin/src/apis/send-message-histories.ts @@ -0,0 +1,29 @@ +import { generateNestParams, handleError, handleSuccess } from '.'; +import axios from '../lib/axios'; + +const BASE_URL = 'send-message-histories'; + +export const getSendMessageHistories = async (params: Record) => { + return await axios({ + url: BASE_URL, + params: generateNestParams(params), + withCredentials: true, + method: 'GET', + }); +}; + +export const sendMessageHistoryTest = async () => { + try { + const { data } = await axios({ + url: `${BASE_URL}/send-test`, + withCredentials: true, + method: 'POST', + }); + + handleSuccess(data); + + return data; + } catch (error) { + handleError(error); + } +}; diff --git a/auto-bid-admin/src/pages/bids.tsx b/auto-bid-admin/src/pages/bids.tsx index 8306053..14d8af0 100644 --- a/auto-bid-admin/src/pages/bids.tsx +++ b/auto-bid-admin/src/pages/bids.tsx @@ -1,6 +1,6 @@ import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import { IconAd, IconAdOff, IconEdit, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react'; +import { IconAd, IconAdOff, IconEdit, IconHammer, IconHistory, IconMenu, IconTrash } from '@tabler/icons-react'; import _ from 'lodash'; import { useMemo, useRef, useState } from 'react'; import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid'; @@ -39,11 +39,7 @@ export default function Bids() { title: 'Model', typeFilter: 'text', }, - // { - // key: 'quantity', - // title: 'Qty', - // typeFilter: 'number', - // }, + { key: 'plus_price', title: 'Plus price', @@ -253,7 +249,7 @@ export default function Bids() { setClickData(row); historiesGraysApiModel.open(); }} - leftSection={} + leftSection={} > Bids diff --git a/auto-bid-admin/src/pages/dashboard.tsx b/auto-bid-admin/src/pages/dashboard.tsx index 5bb8deb..9fc512b 100644 --- a/auto-bid-admin/src/pages/dashboard.tsx +++ b/auto-bid-admin/src/pages/dashboard.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useEffect, useState } from 'react'; -import { Box, Title } from '@mantine/core'; +import { Box, Text, Title } from '@mantine/core'; import io from 'socket.io-client'; import { WorkingPage } from '../components/dashboard'; import { IBid, IWebBid } from '../system/type'; @@ -71,9 +71,13 @@ export default function DashBoard() { - {workingData.map((item, index) => ( - - ))} + {workingData.length > 0 && workingData.map((item, index) => )} + + {workingData.length <= 0 && ( + + No Pages + + )} ); diff --git a/auto-bid-admin/src/pages/send-message-histories.tsx b/auto-bid-admin/src/pages/send-message-histories.tsx new file mode 100644 index 0000000..4fddb4c --- /dev/null +++ b/auto-bid-admin/src/pages/send-message-histories.tsx @@ -0,0 +1,89 @@ +import { Box, Text } from '@mantine/core'; +import { useMemo, useRef } from 'react'; +import { getSendMessageHistories, sendMessageHistoryTest } from '../apis/send-message-histories'; +import Table from '../lib/table/table'; +import { IColumn, TRefTableFn } from '../lib/table/type'; +import { ISendMessageHistory } from '../system/type'; +import { formatTime } from '../utils'; + +export default function SendMessageHistories() { + const refTableFn: TRefTableFn = useRef({}); + + const columns: IColumn[] = [ + { + key: 'id', + title: 'ID', + typeFilter: 'text', + }, + { + key: 'bid', + title: 'Product name', + typeFilter: 'none', + renderRow(row) { + return {row.bid?.name || 'None'}; + }, + }, + { + key: 'message', + title: 'Message', + typeFilter: 'text', + renderRow(row) { + return ; + }, + }, + { + key: 'created_at', + title: 'Create at', + typeFilter: 'none', + renderRow(row) { + return {formatTime(row.created_at)}; + }, + }, + ]; + + const table = useMemo(() => { + return ( + { + await sendMessageHistoryTest(); + }, + }, + ], + }} + refTableFn={refTableFn} + striped + showLoading={true} + highlightOnHover + styleDefaultHead={{ + justifyContent: 'flex-start', + width: 'fit-content', + }} + options={{ + query: getSendMessageHistories, + pathToData: 'data.data', + keyOptions: { + last_page: 'lastPage', + per_page: 'perPage', + from: 'from', + to: 'to', + total: 'total', + }, + }} + rows={[]} + withColumnBorders + showChooses={true} + withTableBorder + columns={columns} + rowKey="id" + /> + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return {table}; +} diff --git a/auto-bid-admin/src/system/links.ts b/auto-bid-admin/src/system/links.ts index d02af08..0696776 100644 --- a/auto-bid-admin/src/system/links.ts +++ b/auto-bid-admin/src/system/links.ts @@ -1,11 +1,13 @@ -import { IconHammer, IconHome2, IconOutlet, IconPageBreak } from '@tabler/icons-react'; +import { IconHammer, IconHome2, IconMessage, IconOutlet, IconPageBreak } from '@tabler/icons-react'; import { Bids, Dashboard, OutBidsLog } from '../pages'; import WebBids from '../pages/web-bids'; +import SendMessageHistories from '../pages/send-message-histories'; export default class Links { public static DASHBOARD = '/dashboard'; public static BIDS = '/bids'; public static WEBS = '/webs'; public static OUT_BIDS_LOG = '/out-bids-log'; + public static SEND_MESSAGE_HISTORIES = '/send-message-histories'; public static HOME = '/'; public static LOGIN = '/login'; @@ -35,5 +37,11 @@ export default class Links { icon: IconOutlet, element: OutBidsLog, }, + { + path: this.SEND_MESSAGE_HISTORIES, + title: 'Send message histories', + icon: IconMessage, + element: SendMessageHistories, + }, ]; } diff --git a/auto-bid-admin/src/system/type/index.ts b/auto-bid-admin/src/system/type/index.ts index 04193f1..879f29e 100644 --- a/auto-bid-admin/src/system/type/index.ts +++ b/auto-bid-admin/src/system/type/index.ts @@ -61,3 +61,8 @@ export interface IPermission extends ITimestamp { name: string; description: string; } +export interface ISendMessageHistory extends ITimestamp { + id: number; + message: string; + bid: IBid; +} 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 7fd6318..bffd5a7 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 @@ -4,6 +4,7 @@ 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'; @Injectable() export class BotTelegramApi { @@ -63,12 +64,18 @@ export class BotTelegramApi { } } - async sendBidInfo(bid: Bid): Promise { - const text = this.formatBidMessage(bid); + async sendBidInfo(bid: Bid): Promise { + try { + const text = this.formatBidMessage(bid); - console.log(text); - await this.sendMessage(text, { - parse_mode: 'HTML', - }); + await this.sendMessage(text, { + parse_mode: 'HTML', + }); + + return true; + } catch (error) { + console.log('SEND MESSAGE FAILURE'); + return false; + } } } diff --git a/auto-bid-server/src/modules/bids/bids.module.ts b/auto-bid-server/src/modules/bids/bids.module.ts index 43ba288..0656ce7 100644 --- a/auto-bid-server/src/modules/bids/bids.module.ts +++ b/auto-bid-server/src/modules/bids/bids.module.ts @@ -19,10 +19,19 @@ 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'; @Module({ imports: [ - TypeOrmModule.forFeature([Bid, BidHistory, OutBidLog, WebBid]), + TypeOrmModule.forFeature([ + Bid, + BidHistory, + OutBidLog, + WebBid, + SendMessageHistory, + ]), EventEmitterModule.forRoot({ wildcard: true, }), @@ -35,6 +44,7 @@ import { GraysApi } from './apis/grays.api'; AdminBidsController, AdminOutBidLogsController, AdminWebBidsController, + AdminSendMessageHistoriesController, ], providers: [ BidsService, @@ -44,6 +54,7 @@ import { GraysApi } from './apis/grays.api'; WebBidsService, BotTelegramApi, GraysApi, + SendMessageHistoriesService, ], }) export class BidsModule {} diff --git a/auto-bid-server/src/modules/bids/controllers/admin/admin-send-message-histories.controller.ts b/auto-bid-server/src/modules/bids/controllers/admin/admin-send-message-histories.controller.ts new file mode 100644 index 0000000..c213ed3 --- /dev/null +++ b/auto-bid-server/src/modules/bids/controllers/admin/admin-send-message-histories.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Post } from '@nestjs/common'; +import { Paginate, PaginateQuery } from 'nestjs-paginate'; +import { SendMessageHistoriesService } from '../../services/send-message-histories.service'; + +@Controller('admin/send-message-histories') +export class AdminSendMessageHistoriesController { + constructor( + private readonly sendMessageService: SendMessageHistoriesService, + ) {} + + @Get() + async index(@Paginate() query: PaginateQuery) { + return await this.sendMessageService.index(query); + } + + @Post('send-test') + async sendTest() { + return await this.sendMessageService.sendTestMessage(); + } +} diff --git a/auto-bid-server/src/modules/bids/entities/bid.entity.ts b/auto-bid-server/src/modules/bids/entities/bid.entity.ts index c1b2d1a..f960c89 100644 --- a/auto-bid-server/src/modules/bids/entities/bid.entity.ts +++ b/auto-bid-server/src/modules/bids/entities/bid.entity.ts @@ -8,6 +8,7 @@ import { import { Timestamp } from './timestamp'; import { BidHistory } from './bid-history.entity'; import { WebBid } from './wed-bid.entity'; +import { SendMessageHistory } from './send-message-histories.entity'; @Entity('bids') export class Bid extends Timestamp { @@ -58,6 +59,11 @@ export class Bid extends Timestamp { }) histories: BidHistory[]; + @OneToMany(() => SendMessageHistory, (sendMessage) => sendMessage.bid, { + cascade: true, + }) + sendMessageHistories: SendMessageHistory[]; + @ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' }) web_bid: WebBid; } 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 new file mode 100644 index 0000000..a691d88 --- /dev/null +++ b/auto-bid-server/src/modules/bids/entities/send-message-histories.entity.ts @@ -0,0 +1,17 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Timestamp } from './timestamp'; +import { Bid } from './bid.entity'; + +@Entity('send_message_histories') +export class SendMessageHistory extends Timestamp { + @PrimaryGeneratedColumn('increment') + id: number; + + @Column({ default: null, nullable: true, type: 'text' }) + message: string; + + @ManyToOne(() => Bid, (bid) => bid.sendMessageHistories, { + onDelete: 'CASCADE', + }) + bid: Bid; +} 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 bb356f2..7ce460c 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 @@ -12,6 +12,7 @@ import { BidHistory } from '../entities/bid-history.entity'; import { Bid } from '../entities/bid.entity'; import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto'; import { BotTelegramApi } from '../apis/bot-telegram.api'; +import { SendMessageHistoriesService } from './send-message-histories.service'; @Injectable() export class BidHistoriesService { @@ -21,6 +22,7 @@ export class BidHistoriesService { @InjectRepository(Bid) readonly bidsRepo: Repository, private readonly botTelegramApi: BotTelegramApi, + readonly sendMessageHistoriesService: SendMessageHistoriesService, ) {} async index() { @@ -72,7 +74,14 @@ export class BidHistoriesService { this.bidsRepo.update(bid_id, { first_bid: false }); } - this.botTelegramApi.sendBidInfo({ ...bid, histories: response }); + const botData = { ...bid, histories: response }; + + this.botTelegramApi.sendBidInfo(botData); + + this.sendMessageHistoriesService.sendMessageRepo.save({ + message: this.botTelegramApi.formatBidMessage(botData), + bid, + }); 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 507e69e..59b8ebb 100644 --- a/auto-bid-server/src/modules/bids/services/bids.service.ts +++ b/auto-bid-server/src/modules/bids/services/bids.service.ts @@ -205,9 +205,12 @@ export class BidsService { bid.status = 'out-bid'; } + console.log('Update ' + id); + const result = await this.bidsRepo.save({ ...bid, ...data, + updated_at: new Date(), }); this.emitAllBidEvent(); diff --git a/auto-bid-server/src/modules/bids/services/send-message-histories.service.ts b/auto-bid-server/src/modules/bids/services/send-message-histories.service.ts new file mode 100644 index 0000000..07ca546 --- /dev/null +++ b/auto-bid-server/src/modules/bids/services/send-message-histories.service.ts @@ -0,0 +1,106 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { OutBidLog } from '../entities/out-bid-log.entity'; +import AppResponse from 'src/response/app-response'; +import { + FilterOperator, + FilterSuffix, + paginate, + PaginateQuery, +} from 'nestjs-paginate'; +import { Column } from 'nestjs-paginate/lib/helper'; +import { CreateOutBidLogDto } from '../dto/out-bid-log/create-out-bid-log.dto'; +import { SendMessageHistory } from '../entities/send-message-histories.entity'; +import { BotTelegramApi } from '../apis/bot-telegram.api'; +import { BidsService } from './bids.service'; + +@Injectable() +export class SendMessageHistoriesService { + constructor( + @InjectRepository(SendMessageHistory) + readonly sendMessageRepo: Repository, + private readonly botTelegramApi: BotTelegramApi, + private readonly bidsService: BidsService, + ) {} + + async index(query: PaginateQuery) { + const filterableColumns: { + [key in Column | (string & {})]?: + | (FilterOperator | FilterSuffix)[] + | true; + } = { + id: true, + message: true, + 'bid.name': true, + 'bid.lot_id': true, + }; + + query.filter = AppResponse.processFilters(query.filter, filterableColumns); + + const data = await paginate(query, this.sendMessageRepo, { + sortableColumns: ['id', 'message', 'bid.name'], + searchableColumns: ['id', 'message', 'bid.name'], + defaultLimit: 15, + filterableColumns, + defaultSortBy: [['id', 'DESC']], + relations: { + bid: true, + }, + maxLimit: 100, + }); + + return AppResponse.toPagination( + data, + true, + SendMessageHistory, + ); + } + + async sendTestMessage() { + const date = new Date().toUTCString(); + + const mockBid = this.bidsService.bidsRepo.create({ + id: 12345, + max_price: 5000, + reserve_price: 1000, + current_price: 1200, + name: 'Laptop Gaming ASUS ROG', + quantity: 1, + url: 'https://example.com/product/12345', + model: 'ROG Strix G15', + lot_id: 'LOT-67890', + plus_price: 50, + close_time: date, + start_bid_time: date, + first_bid: false, + status: 'biding', + histories: [ + { + id: 1, + price: 1000, + created_at: date, + updated_at: date, + }, + { + id: 2, + price: 1100, + created_at: date, + updated_at: date, + }, + { + id: 3, + price: 1200, + created_at: date, + updated_at: date, + }, + ], + created_at: date, + updated_at: date, + }); + + const result = await this.botTelegramApi.sendBidInfo(mockBid); + + return AppResponse.toResponse(result); + } +} diff --git a/auto-bid-tool/data/fake-out-lots.json b/auto-bid-tool/data/fake-out-lots.json index 7dbfc44..763ea36 100644 --- a/auto-bid-tool/data/fake-out-lots.json +++ b/auto-bid-tool/data/fake-out-lots.json @@ -1,14 +1,9 @@ { "AuctionOutBidLots": [ { - "Id": "23464489", - "Sku": "0002-2566250", - "Bid": "AU $59" - }, - { - "Id": "23478598", - "Sku": "0153-3032503", - "Bid": "AU $12" + "Id": "23482715", + "Sku": "0045-10734155", + "Bid": "AU $50" } ], "NumOfLosingBids": 3 diff --git a/auto-bid-tool/index.js b/auto-bid-tool/index.js index b2d6204..b9a91a6 100644 --- a/auto-bid-tool/index.js +++ b/auto-bid-tool/index.js @@ -1,7 +1,7 @@ import 'dotenv/config'; import _ from 'lodash'; import { io } from 'socket.io-client'; -import { createApiBid, createBidProduct, deleteProfile } from './service/app-service.js'; +import { createApiBid, createBidProduct, deleteProfile, shouldUpdateProductTab } from './service/app-service.js'; import browser from './system/browser.js'; import configs from './system/config.js'; import { isTimeReached, safeClosePage } from './system/utils.js'; @@ -48,6 +48,84 @@ const handleUpdateProductTabs = (data) => { MANAGER_BIDS = newDataManager; }; +// const tracking = async () => { +// if (_INTERVAL_TRACKING_ID) { +// clearInterval(_INTERVAL_TRACKING_ID); +// _INTERVAL_TRACKING_ID = null; +// } + +// _INTERVAL_TRACKING_ID = setInterval(async () => { +// const productTabs = _.flatMap(MANAGER_BIDS, 'children'); + +// for (const productTab of productTabs) { +// if (!productTab.parent_browser_context) { +// const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id }); + +// productTab.parent_browser_context = parent.browser_context; + +// if (!productTab.parent_browser_context) { +// console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`); +// continue; +// } +// } + +// if (!productTab.first_bid) { +// console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`); + +// const updatedAt = new Date(productTab.updated_at).getTime(); +// const now = Date.now(); + +// if (!productTab.page_context) { +// await productTab.puppeteer_connect(); +// } + +// if (productTab.page_context.url() !== productTab.url) { +// await productTab.gotoLink(); +// } + +// if (now - updatedAt < ONE_MINUTE) { +// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`); +// } + +// await productTab.update(); +// console.log(`🔄 Updating Product ID: ${productTab.id}...`); + +// continue; +// } + +// if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) { +// console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`); + +// const updatedAt = new Date(productTab.updated_at).getTime(); +// const now = Date.now(); + +// if (!productTab.page_context) { +// await productTab.puppeteer_connect(); +// } + +// if (productTab.page_context.url() !== productTab.url) { +// await productTab.gotoLink(); +// } + +// if (now - updatedAt < ONE_MINUTE) { +// console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`); +// } + +// await productTab.update(); +// continue; +// } + +// if (!productTab.page_context) { +// console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`); +// await productTab.puppeteer_connect(); +// } + +// console.log(`🚀 Executing action for Product ID: ${productTab.id}`); +// await productTab.action(); +// } +// }, configs.AUTO_TRACKING_DELAY); +// }; + const tracking = async () => { if (_INTERVAL_TRACKING_ID) { clearInterval(_INTERVAL_TRACKING_ID); @@ -58,10 +136,10 @@ const tracking = async () => { const productTabs = _.flatMap(MANAGER_BIDS, 'children'); for (const productTab of productTabs) { + // Tìm parent context nếu chưa có if (!productTab.parent_browser_context) { const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id }); - - productTab.parent_browser_context = parent.browser_context; + productTab.parent_browser_context = parent?.browser_context; if (!productTab.parent_browser_context) { console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`); @@ -69,85 +147,43 @@ const tracking = async () => { } } - if (!productTab.first_bid) { - console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`); - - if (!productTab.page_context) { - console.log(`🔌 Establishing connection for Product ID: ${productTab.id}`); - await productTab.puppeteer_connect(); - - await productTab.handleTakeWorkSnapshot(); - } - - continue; - } - - if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) { - console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`); - continue; - } - + // Kết nối Puppeteer nếu chưa có page_context if (!productTab.page_context) { console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`); await productTab.puppeteer_connect(); } + // Nếu URL thay đổi, điều hướng đến URL mới + if (productTab.page_context.url() !== productTab.url) { + await productTab.gotoLink(); + } + + // Kiểm tra nếu cần cập nhật trước khi gọi update() + if (shouldUpdateProductTab(productTab)) { + console.log(`🔄 Updating Product ID: ${productTab.id}...`); + await productTab.update(); + } else { + console.log(`⏳ Product ID: ${productTab.id} was updated recently. Skipping update.`); + } + + // Nếu chưa có first_bid (trạng thái chưa đặt giá) + if (!productTab.first_bid) { + console.log(`🎯 Tracking out-bid event for Product ID: ${productTab.id}`); + continue; + } + + // Nếu chưa đến giờ bid + if (productTab.start_bid_time && !isTimeReached(productTab.start_bid_time)) { + console.log(`⏳ Not yet time to bid. Skipping Product ID: ${productTab.id}`); + continue; + } + console.log(`🚀 Executing action for Product ID: ${productTab.id}`); await productTab.action(); } }, configs.AUTO_TRACKING_DELAY); }; -// const clearLazyTab = async () => { -// if (_CLEAR_LAZY_TAB_ID) { -// clearInterval(_CLEAR_LAZY_TAB_ID); -// _CLEAR_LAZY_TAB_ID = null; // Reset tránh memory leak -// } - -// try { -// _CLEAR_LAZY_TAB_ID = setInterval(async () => { -// if (!browser) { -// console.warn('⚠️ Browser is not available.'); -// return; -// } - -// try { -// const pages = await browser.pages(); - -// // Lấy danh sách URL từ flattenedArray -// const activeUrls = _.flatMap(MANAGER_BIDS, (item) => [ -// // item.url, -// ...item.children.map((child) => child.url), -// ]).filter(Boolean); // Lọc bỏ null hoặc undefined - -// console.log( -// '🔍 Page URLs:', -// pages.map((page) => page.url()), -// ); - -// for (const page of pages) { -// const pageUrl = page.url(); - -// if (!activeUrls.includes(pageUrl)) { -// if (!page.isClosed()) { -// try { -// await page.close(); -// console.log(`🛑 Closing unused tab: ${pageUrl}`); -// } catch (err) { -// console.warn(`⚠️ Error closing tab ${pageUrl}:`, err.message); -// } -// } -// } -// } -// } catch (err) { -// console.error('❌ Error in clearLazyTab:', err.message); -// } -// }, configs.AUTO_TRACKING_CLEANING); -// } catch (error) { -// console.log('CLEAR LAZY TAB: ', error.message); -// } -// }; - const clearLazyTab = async () => { if (_CLEAR_LAZY_TAB_ID) { clearInterval(_CLEAR_LAZY_TAB_ID); 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 370d69a..765a698 100644 --- a/auto-bid-tool/models/grays.com/grays-api-bid.js +++ b/auto-bid-tool/models/grays.com/grays-api-bid.js @@ -15,29 +15,29 @@ export class GrayApiBid extends ApiBid { async polling(page) { try { - // // 🔥 Xóa tất cả event chặn request trước khi thêm mới - // page.removeAllListeners('request'); - // await page.setRequestInterception(true); + // 🔥 Xóa tất cả event chặn request trước khi thêm mới + page.removeAllListeners('request'); + await page.setRequestInterception(true); - // page.on('request', (request) => { - // if (request.url().includes('api/Notifications/GetOutBidLots')) { - // console.log('🚀 Fake response cho request:', request.url()); + page.on('request', (request) => { + if (request.url().includes('api/Notifications/GetOutBidLots')) { + console.log('🚀 Fake response cho request:', request.url()); - // const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8'); + const fakeData = fs.readFileSync('./data/fake-out-lots.json', 'utf8'); - // request.respond({ - // status: 200, - // contentType: 'application/json', - // body: fakeData, - // }); - // } else { - // try { - // request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn - // } catch (error) { - // console.error('⚠️ Lỗi khi tiếp tục request:', error.message); - // } - // } - // }); + request.respond({ + status: 200, + contentType: 'application/json', + body: fakeData, + }); + } else { + try { + request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn + } catch (error) { + console.error('⚠️ Lỗi khi tiếp tục request:', error.message); + } + } + }); console.log('🔄 Starting polling process...'); 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 c94e920..5ece937 100644 --- a/auto-bid-tool/models/grays.com/grays-product-bid.js +++ b/auto-bid-tool/models/grays.com/grays-product-bid.js @@ -1,6 +1,6 @@ import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js'; import CONSTANTS from '../../system/constants.js'; -import { delay, extractNumber, isNumber, isTimeReached, safeClosePage, takeSnapshot } from '../../system/utils.js'; +import { delay, extractNumber, isNumber, isTimeReached, removeFalsyValues, safeClosePage, takeSnapshot } from '../../system/utils.js'; import { ProductBid } from '../product-bid.js'; export class GraysProductBid extends ProductBid { @@ -17,7 +17,7 @@ export class GraysProductBid extends ProductBid { if (!isNumber(price_value)) { console.log("Can't get PRICE_VALUE ❌"); await takeSnapshot(page, this, 'price-value-null'); - await safeClosePage(this); + // await safeClosePage(this); return { result: false, bid_price: 0 }; } @@ -27,7 +27,7 @@ export class GraysProductBid extends ProductBid { if (bid_price > this.max_price) { console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌'); await takeSnapshot(page, this, 'price-bid-more-than'); - await safeClosePage(this); + // await safeClosePage(this); await outBid(this.id); @@ -41,7 +41,7 @@ export class GraysProductBid extends ProductBid { if (!response.status) { // await this.handleReturnProductPage(page); - await safeClosePage(this); + // await safeClosePage(this); return { result: false, bid_price: 0 }; } @@ -97,7 +97,7 @@ export class GraysProductBid extends ProductBid { if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) { console.log(`Product is close ${close_time} ❌`); - await safeClosePage(this); + // await safeClosePage(this); return { result: true, close_time }; } @@ -131,7 +131,7 @@ export class GraysProductBid extends ProductBid { console.log({ error: error.message }); console.log('❌ Timeout to loading'); await takeSnapshot(page, this, 'timeout to loading'); - await safeClosePage(this); + // await safeClosePage(this); return false; } } @@ -142,7 +142,7 @@ export class GraysProductBid extends ProductBid { } async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) { - if (close_time && this.close_time == close_time) return; + // if (close_time && this.close_time == close_time) return; const response = await updateBid(this.id, { lot_id, close_time, name, current_price, reserve_price: Number(reserve_price) || 0 }); @@ -153,19 +153,75 @@ export class GraysProductBid extends ProductBid { } } + // update = async () => { + // if (!this.page_context) return; + + // const page = this.page_context; + + // const close_time = await this.getCloseTime(); + // const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null; + // const lot_id = await page.$eval('#lotId', (el) => el.value); + // const name = (await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText)) || null; + // const current_price = + // (await page.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)) || null; + + // console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price ${price_value}`); + + // this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null }); + + // return { price_value, lot_id, name, current_price }; + // }; + update = async () => { + if (!this.page_context) return; + + const page = this.page_context; + + try { + const close_time = await this.getCloseTime(); + + // Chờ phần tử xuất hiện trước khi lấy giá trị + await page.waitForSelector('#priceValue', { timeout: 5000 }).catch(() => null); + const price_value = await page.$eval('#priceValue', (el) => el.value).catch(() => null); + + await page.waitForSelector('#lotId', { timeout: 5000 }).catch(() => null); + const lot_id = await page.$eval('#lotId', (el) => el.value).catch(() => null); + + await page.waitForSelector('#placebid-sticky > div:nth-child(2) > div > h3', { timeout: 5000 }).catch(() => null); + const name = await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText).catch(() => null); + + await page + .waitForSelector('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', { timeout: 5000 }) + .catch(() => null); + const current_price = await page + .$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText) + .catch(() => null); + + console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price: ${price_value}`); + + const data = removeFalsyValues({ + lot_id, + reserve_price: price_value, + close_time: String(close_time), + name, + current_price: current_price ? extractNumber(current_price) : null, + }); + + this.handleUpdateBid(data); + + return { price_value, lot_id, name, current_price }; + } catch (error) { + console.error(`🚨 Error updating product info: ${error.message}`); + return null; + } + }; + action = async () => { try { const page = this.page_context; - 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.'); + await this.gotoLink(); await delay(1000); - // this.handleTakeWorkSnapshot(); const { close_time, ...isCloseProduct } = await this.isCloseProduct(page); if (isCloseProduct.result) { @@ -175,15 +231,9 @@ export class GraysProductBid extends ProductBid { await delay(500); - const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null; - const lot_id = await page.$eval('#lotId', (el) => el.value); - const name = (await page.$eval('#placebid-sticky > div:nth-child(2) > div > h3', (el) => el.innerText)) || null; - const current_price = - (await page.$eval('#biddableLot > form > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > span > span', (el) => el.innerText)) || null; + const { price_value } = await this.update(); - console.log(`📌 Product Info: Lot ID: ${lot_id}, Name: ${name}, Current Price: ${current_price}, Reserve price ${price_value}`); - - this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null }); + if (!price_value) return; const { result, bid_price } = await this.validate({ page, price_value }); @@ -203,7 +253,7 @@ export class GraysProductBid extends ProductBid { if (!resultPlaceBid) { console.log('❌ Error occurred while placing the bid.'); await takeSnapshot(page, this, 'place-bid-action'); - await safeClosePage(this); + // await safeClosePage(this); return; } @@ -212,7 +262,7 @@ export class GraysProductBid extends ProductBid { await this.handleReturnProductPage(page); } catch (error) { console.error(`🚨 Error navigating the page: ${error.message}`); - safeClosePage(this); + // safeClosePage(this); } }; } diff --git a/auto-bid-tool/models/product-bid.js b/auto-bid-tool/models/product-bid.js index 710f99e..06cfef3 100644 --- a/auto-bid-tool/models/product-bid.js +++ b/auto-bid-tool/models/product-bid.js @@ -24,6 +24,7 @@ export class ProductBid extends Bid { current_price; name; reserve_price; + update; constructor({ url, @@ -133,4 +134,15 @@ 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.'); + } } diff --git a/auto-bid-tool/service/app-service.js b/auto-bid-tool/service/app-service.js index a698bfe..f32fd42 100644 --- a/auto-bid-tool/service/app-service.js +++ b/auto-bid-tool/service/app-service.js @@ -5,6 +5,9 @@ import configs from '../system/config.js'; import CONSTANTS from '../system/constants.js'; import { sanitizeFileName } from '../system/utils.js'; import * as fs from 'fs'; + +const ONE_MINUTE = 60 * 1000; + export const handleCloseRemoveProduct = (data) => { if (!Array.isArray(data)) return; @@ -41,3 +44,9 @@ export const deleteProfile = (data) => { return false; }; + +export const shouldUpdateProductTab = (productTab) => { + const updatedAt = new Date(productTab.updated_at).getTime(); + const now = Date.now(); + return now - updatedAt >= ONE_MINUTE; +}; diff --git a/auto-bid-tool/system/apis/bid.js b/auto-bid-tool/system/apis/bid.js index 7ed6a83..bae85f1 100644 --- a/auto-bid-tool/system/apis/bid.js +++ b/auto-bid-tool/system/apis/bid.js @@ -84,7 +84,7 @@ export const updateStatusByPrice = async (id, current_price) => { method: 'POST', url: 'bids/update-status/' + id, data: { - current_price, + current_price: Number(current_price) | 0, }, }); diff --git a/auto-bid-tool/system/apis/out-bid-log.js b/auto-bid-tool/system/apis/out-bid-log.js index a8e35d3..76e8e06 100644 --- a/auto-bid-tool/system/apis/out-bid-log.js +++ b/auto-bid-tool/system/apis/out-bid-log.js @@ -17,7 +17,7 @@ export const createOutBidLog = async (values) => { return data.data; } catch (error) { - console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error); + console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error.message); return false; } }; diff --git a/auto-bid-tool/system/browser.js b/auto-bid-tool/system/browser.js index 9d9015d..66c230e 100644 --- a/auto-bid-tool/system/browser.js +++ b/auto-bid-tool/system/browser.js @@ -7,6 +7,7 @@ puppeteer.use(StealthPlugin()); const browser = await puppeteer.launch({ headless: process.env.ENVIRONMENT === 'prod' ? true : false, // userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile + timeout: 60000, args: [ '--no-sandbox', '--disable-setuid-sandbox', diff --git a/auto-bid-tool/system/utils.js b/auto-bid-tool/system/utils.js index a97f7d5..6d52f84 100644 --- a/auto-bid-tool/system/utils.js +++ b/auto-bid-tool/system/utils.js @@ -6,7 +6,7 @@ import { updateStatusWork } from './apis/bid.js'; export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value)); export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => { - if (page.isClosed()) return; + if (!page || page.isClosed()) return; try { const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id @@ -23,7 +23,19 @@ export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_ console.log(`📂 Save at folder: ${typeDir}`); } - await page.waitForSelector('body', { visible: true, timeout: 5000 }); + // await page.waitForSelector('body', { visible: true, timeout: 5000 }); + // Kiểm tra có thể điều hướng trang không + const isPageResponsive = await page.evaluate(() => document.readyState === 'complete'); + if (!isPageResponsive) { + console.log('🚫 Page is unresponsive, skipping snapshot.'); + return; + } + + // Chờ tối đa 15 giây, nếu không thấy thì bỏ qua + await page.waitForSelector('body', { visible: true, timeout: 15000 }).catch(() => { + console.log('⚠️ Body selector not found, skipping snapshot.'); + return; + }); // Chụp ảnh màn hình và lưu vào filePath await page.screenshot({ path: filePath, fullPage: true }); @@ -79,3 +91,12 @@ export const sanitizeFileName = (url) => { export const getPathProfile = (origin_url) => { return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json'); }; + +export function removeFalsyValues(obj, excludeKeys = []) { + return Object.entries(obj).reduce((acc, [key, value]) => { + if (value || excludeKeys.includes(key)) { + acc[key] = value; + } + return acc; + }, {}); +}