This commit is contained in:
nkhangg 2025-03-19 14:38:56 +07:00
parent fff5447ce1
commit b1e34be9ce
24 changed files with 581 additions and 147 deletions

View File

@ -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<string, string | number>) => {
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);
}
};

View File

@ -1,6 +1,6 @@
import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core'; import { ActionIcon, Badge, Box, Menu, Text, Tooltip } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; 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 _ from 'lodash';
import { useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid'; import { deleteBid, deletesBid, getBids, toggleBid } from '../apis/bid';
@ -39,11 +39,7 @@ export default function Bids() {
title: 'Model', title: 'Model',
typeFilter: 'text', typeFilter: 'text',
}, },
// {
// key: 'quantity',
// title: 'Qty',
// typeFilter: 'number',
// },
{ {
key: 'plus_price', key: 'plus_price',
title: 'Plus price', title: 'Plus price',
@ -253,7 +249,7 @@ export default function Bids() {
setClickData(row); setClickData(row);
historiesGraysApiModel.open(); historiesGraysApiModel.open();
}} }}
leftSection={<IconHistory size={14} />} leftSection={<IconHammer size={14} />}
> >
Bids Bids
</Menu.Item> </Menu.Item>

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from 'react'; 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 io from 'socket.io-client';
import { WorkingPage } from '../components/dashboard'; import { WorkingPage } from '../components/dashboard';
import { IBid, IWebBid } from '../system/type'; import { IBid, IWebBid } from '../system/type';
@ -71,9 +71,13 @@ export default function DashBoard() {
</Title> </Title>
<Box className="grid grid-cols-4 gap-4"> <Box className="grid grid-cols-4 gap-4">
{workingData.map((item, index) => ( {workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
<WorkingPage socket={socket} data={item} key={item.id + index} />
))} {workingData.length <= 0 && (
<Box className="flex items-center justify-center col-span-4">
<Text>No Pages</Text>
</Box>
)}
</Box> </Box>
</Box> </Box>
); );

View File

@ -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<ISendMessageHistory> = useRef({});
const columns: IColumn<ISendMessageHistory>[] = [
{
key: 'id',
title: 'ID',
typeFilter: 'text',
},
{
key: 'bid',
title: 'Product name',
typeFilter: 'none',
renderRow(row) {
return <Text>{row.bid?.name || 'None'}</Text>;
},
},
{
key: 'message',
title: 'Message',
typeFilter: 'text',
renderRow(row) {
return <Box className="max-w-[500px]" dangerouslySetInnerHTML={{ __html: row.message }}></Box>;
},
},
{
key: 'created_at',
title: 'Create at',
typeFilter: 'none',
renderRow(row) {
return <span className="text-sm">{formatTime(row.created_at)}</span>;
},
},
];
const table = useMemo(() => {
return (
<Table
actionsOptions={{
actions: [
{
key: 'send-test',
title: 'Send test',
callback: async () => {
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 <Box>{table}</Box>;
}

View File

@ -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 { Bids, Dashboard, OutBidsLog } from '../pages';
import WebBids from '../pages/web-bids'; import WebBids from '../pages/web-bids';
import SendMessageHistories from '../pages/send-message-histories';
export default class Links { export default class Links {
public static DASHBOARD = '/dashboard'; public static DASHBOARD = '/dashboard';
public static BIDS = '/bids'; public static BIDS = '/bids';
public static WEBS = '/webs'; public static WEBS = '/webs';
public static OUT_BIDS_LOG = '/out-bids-log'; public static OUT_BIDS_LOG = '/out-bids-log';
public static SEND_MESSAGE_HISTORIES = '/send-message-histories';
public static HOME = '/'; public static HOME = '/';
public static LOGIN = '/login'; public static LOGIN = '/login';
@ -35,5 +37,11 @@ export default class Links {
icon: IconOutlet, icon: IconOutlet,
element: OutBidsLog, element: OutBidsLog,
}, },
{
path: this.SEND_MESSAGE_HISTORIES,
title: 'Send message histories',
icon: IconMessage,
element: SendMessageHistories,
},
]; ];
} }

View File

@ -61,3 +61,8 @@ export interface IPermission extends ITimestamp {
name: string; name: string;
description: string; description: string;
} }
export interface ISendMessageHistory extends ITimestamp {
id: number;
message: string;
bid: IBid;
}

View File

@ -4,6 +4,7 @@ import axios from 'axios';
import { escapeMarkdownV2 } from 'src/ultils'; import { escapeMarkdownV2 } from 'src/ultils';
import { Bid } from '../entities/bid.entity'; import { Bid } from '../entities/bid.entity';
import * as dayjs from 'dayjs'; import * as dayjs from 'dayjs';
import { SendMessageHistoriesService } from '../services/send-message-histories.service';
@Injectable() @Injectable()
export class BotTelegramApi { export class BotTelegramApi {
@ -63,12 +64,18 @@ export class BotTelegramApi {
} }
} }
async sendBidInfo(bid: Bid): Promise<void> { async sendBidInfo(bid: Bid): Promise<boolean> {
const text = this.formatBidMessage(bid); try {
const text = this.formatBidMessage(bid);
console.log(text); await this.sendMessage(text, {
await this.sendMessage(text, { parse_mode: 'HTML',
parse_mode: 'HTML', });
});
return true;
} catch (error) {
console.log('SEND MESSAGE FAILURE');
return false;
}
} }
} }

View File

@ -19,10 +19,19 @@ import { OutBidLogsService } from './services/out-bid-logs.service';
import { WebBidsService } from './services/web-bids.service'; import { WebBidsService } from './services/web-bids.service';
import { BotTelegramApi } from './apis/bot-telegram.api'; import { BotTelegramApi } from './apis/bot-telegram.api';
import { GraysApi } from './apis/grays.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({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Bid, BidHistory, OutBidLog, WebBid]), TypeOrmModule.forFeature([
Bid,
BidHistory,
OutBidLog,
WebBid,
SendMessageHistory,
]),
EventEmitterModule.forRoot({ EventEmitterModule.forRoot({
wildcard: true, wildcard: true,
}), }),
@ -35,6 +44,7 @@ import { GraysApi } from './apis/grays.api';
AdminBidsController, AdminBidsController,
AdminOutBidLogsController, AdminOutBidLogsController,
AdminWebBidsController, AdminWebBidsController,
AdminSendMessageHistoriesController,
], ],
providers: [ providers: [
BidsService, BidsService,
@ -44,6 +54,7 @@ import { GraysApi } from './apis/grays.api';
WebBidsService, WebBidsService,
BotTelegramApi, BotTelegramApi,
GraysApi, GraysApi,
SendMessageHistoriesService,
], ],
}) })
export class BidsModule {} export class BidsModule {}

View File

@ -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();
}
}

View File

@ -8,6 +8,7 @@ import {
import { Timestamp } from './timestamp'; import { Timestamp } from './timestamp';
import { BidHistory } from './bid-history.entity'; import { BidHistory } from './bid-history.entity';
import { WebBid } from './wed-bid.entity'; import { WebBid } from './wed-bid.entity';
import { SendMessageHistory } from './send-message-histories.entity';
@Entity('bids') @Entity('bids')
export class Bid extends Timestamp { export class Bid extends Timestamp {
@ -58,6 +59,11 @@ export class Bid extends Timestamp {
}) })
histories: BidHistory[]; histories: BidHistory[];
@OneToMany(() => SendMessageHistory, (sendMessage) => sendMessage.bid, {
cascade: true,
})
sendMessageHistories: SendMessageHistory[];
@ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' }) @ManyToOne(() => WebBid, (web) => web.children, { onDelete: 'CASCADE' })
web_bid: WebBid; web_bid: WebBid;
} }

View File

@ -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;
}

View File

@ -12,6 +12,7 @@ import { BidHistory } from '../entities/bid-history.entity';
import { Bid } from '../entities/bid.entity'; import { Bid } from '../entities/bid.entity';
import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto'; import { CreateBidHistoryDto } from '../dto/bid-history/create-bid-history.dto';
import { BotTelegramApi } from '../apis/bot-telegram.api'; import { BotTelegramApi } from '../apis/bot-telegram.api';
import { SendMessageHistoriesService } from './send-message-histories.service';
@Injectable() @Injectable()
export class BidHistoriesService { export class BidHistoriesService {
@ -21,6 +22,7 @@ export class BidHistoriesService {
@InjectRepository(Bid) @InjectRepository(Bid)
readonly bidsRepo: Repository<Bid>, readonly bidsRepo: Repository<Bid>,
private readonly botTelegramApi: BotTelegramApi, private readonly botTelegramApi: BotTelegramApi,
readonly sendMessageHistoriesService: SendMessageHistoriesService,
) {} ) {}
async index() { async index() {
@ -72,7 +74,14 @@ export class BidHistoriesService {
this.bidsRepo.update(bid_id, { first_bid: false }); 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)); return AppResponse.toResponse(plainToClass(BidHistory, response));
} }

View File

@ -205,9 +205,12 @@ export class BidsService {
bid.status = 'out-bid'; bid.status = 'out-bid';
} }
console.log('Update ' + id);
const result = await this.bidsRepo.save({ const result = await this.bidsRepo.save({
...bid, ...bid,
...data, ...data,
updated_at: new Date(),
}); });
this.emitAllBidEvent(); this.emitAllBidEvent();

View File

@ -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<SendMessageHistory>,
private readonly botTelegramApi: BotTelegramApi,
private readonly bidsService: BidsService,
) {}
async index(query: PaginateQuery) {
const filterableColumns: {
[key in Column<SendMessageHistory> | (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<SendMessageHistory>(
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);
}
}

View File

@ -1,14 +1,9 @@
{ {
"AuctionOutBidLots": [ "AuctionOutBidLots": [
{ {
"Id": "23464489", "Id": "23482715",
"Sku": "0002-2566250", "Sku": "0045-10734155",
"Bid": "AU $59" "Bid": "AU $50"
},
{
"Id": "23478598",
"Sku": "0153-3032503",
"Bid": "AU $12"
} }
], ],
"NumOfLosingBids": 3 "NumOfLosingBids": 3

View File

@ -1,7 +1,7 @@
import 'dotenv/config'; import 'dotenv/config';
import _ from 'lodash'; import _ from 'lodash';
import { io } from 'socket.io-client'; 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 browser from './system/browser.js';
import configs from './system/config.js'; import configs from './system/config.js';
import { isTimeReached, safeClosePage } from './system/utils.js'; import { isTimeReached, safeClosePage } from './system/utils.js';
@ -48,6 +48,84 @@ const handleUpdateProductTabs = (data) => {
MANAGER_BIDS = newDataManager; 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 () => { const tracking = async () => {
if (_INTERVAL_TRACKING_ID) { if (_INTERVAL_TRACKING_ID) {
clearInterval(_INTERVAL_TRACKING_ID); clearInterval(_INTERVAL_TRACKING_ID);
@ -58,10 +136,10 @@ const tracking = async () => {
const productTabs = _.flatMap(MANAGER_BIDS, 'children'); const productTabs = _.flatMap(MANAGER_BIDS, 'children');
for (const productTab of productTabs) { for (const productTab of productTabs) {
// Tìm parent context nếu chưa có
if (!productTab.parent_browser_context) { if (!productTab.parent_browser_context) {
const parent = _.find(MANAGER_BIDS, { id: productTab.web_bid.id }); 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) { if (!productTab.parent_browser_context) {
console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`); console.log(`🔄 Waiting for parent process to start... (Product ID: ${productTab.id})`);
@ -69,85 +147,43 @@ const tracking = async () => {
} }
} }
if (!productTab.first_bid) { // Kết nối Puppeteer nếu chưa có page_context
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;
}
if (!productTab.page_context) { if (!productTab.page_context) {
console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`); console.log(`🔌 Connecting to page for Product ID: ${productTab.id}`);
await productTab.puppeteer_connect(); 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}`); console.log(`🚀 Executing action for Product ID: ${productTab.id}`);
await productTab.action(); await productTab.action();
} }
}, configs.AUTO_TRACKING_DELAY); }, 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 () => { const clearLazyTab = async () => {
if (_CLEAR_LAZY_TAB_ID) { if (_CLEAR_LAZY_TAB_ID) {
clearInterval(_CLEAR_LAZY_TAB_ID); clearInterval(_CLEAR_LAZY_TAB_ID);

View File

@ -15,29 +15,29 @@ export class GrayApiBid extends ApiBid {
async polling(page) { async polling(page) {
try { try {
// // 🔥 Xóa tất cả event chặn request trước khi thêm mới // 🔥 Xóa tất cả event chặn request trước khi thêm mới
// page.removeAllListeners('request'); page.removeAllListeners('request');
// await page.setRequestInterception(true); await page.setRequestInterception(true);
// page.on('request', (request) => { page.on('request', (request) => {
// if (request.url().includes('api/Notifications/GetOutBidLots')) { if (request.url().includes('api/Notifications/GetOutBidLots')) {
// console.log('🚀 Fake response cho request:', request.url()); 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({ request.respond({
// status: 200, status: 200,
// contentType: 'application/json', contentType: 'application/json',
// body: fakeData, body: fakeData,
// }); });
// } else { } else {
// try { try {
// request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn request.continue(); // ⚠️ Chỉ tiếp tục nếu request chưa bị chặn
// } catch (error) { } catch (error) {
// console.error('⚠️ Lỗi khi tiếp tục request:', error.message); console.error('⚠️ Lỗi khi tiếp tục request:', error.message);
// } }
// } }
// }); });
console.log('🔄 Starting polling process...'); console.log('🔄 Starting polling process...');

View File

@ -1,6 +1,6 @@
import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js'; import { outBid, pushPrice, updateBid, updateStatusByPrice } from '../../system/apis/bid.js';
import CONSTANTS from '../../system/constants.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'; import { ProductBid } from '../product-bid.js';
export class GraysProductBid extends ProductBid { export class GraysProductBid extends ProductBid {
@ -17,7 +17,7 @@ export class GraysProductBid extends ProductBid {
if (!isNumber(price_value)) { if (!isNumber(price_value)) {
console.log("Can't get PRICE_VALUE ❌"); console.log("Can't get PRICE_VALUE ❌");
await takeSnapshot(page, this, 'price-value-null'); await takeSnapshot(page, this, 'price-value-null');
await safeClosePage(this); // await safeClosePage(this);
return { result: false, bid_price: 0 }; return { result: false, bid_price: 0 };
} }
@ -27,7 +27,7 @@ export class GraysProductBid extends ProductBid {
if (bid_price > this.max_price) { if (bid_price > this.max_price) {
console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌'); console.log('PRICE BID is more than MAX_VALUE => STOP BID THIS PRODUCT ❌');
await takeSnapshot(page, this, 'price-bid-more-than'); await takeSnapshot(page, this, 'price-bid-more-than');
await safeClosePage(this); // await safeClosePage(this);
await outBid(this.id); await outBid(this.id);
@ -41,7 +41,7 @@ export class GraysProductBid extends ProductBid {
if (!response.status) { if (!response.status) {
// await this.handleReturnProductPage(page); // await this.handleReturnProductPage(page);
await safeClosePage(this); // await safeClosePage(this);
return { result: false, bid_price: 0 }; 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()) { if (!close_time || new Date(close_time).getTime() <= new Date().getTime()) {
console.log(`Product is close ${close_time}`); console.log(`Product is close ${close_time}`);
await safeClosePage(this); // await safeClosePage(this);
return { result: true, close_time }; return { result: true, close_time };
} }
@ -131,7 +131,7 @@ export class GraysProductBid extends ProductBid {
console.log({ error: error.message }); console.log({ error: error.message });
console.log('❌ Timeout to loading'); console.log('❌ Timeout to loading');
await takeSnapshot(page, this, 'timeout to loading'); await takeSnapshot(page, this, 'timeout to loading');
await safeClosePage(this); // await safeClosePage(this);
return false; return false;
} }
} }
@ -142,7 +142,7 @@ export class GraysProductBid extends ProductBid {
} }
async handleUpdateBid({ lot_id, close_time, name, current_price, reserve_price }) { 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 }); 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 () => { action = async () => {
try { try {
const page = this.page_context; const page = this.page_context;
console.log('🔄 Starting the bidding process...');
await page.goto(this.url, { waitUntil: 'networkidle2' }); await this.gotoLink();
console.log(`✅ Navigated to: ${this.url}`);
await page.bringToFront();
console.log('👀 Brought the tab to the foreground.');
await delay(1000); await delay(1000);
// this.handleTakeWorkSnapshot();
const { close_time, ...isCloseProduct } = await this.isCloseProduct(page); const { close_time, ...isCloseProduct } = await this.isCloseProduct(page);
if (isCloseProduct.result) { if (isCloseProduct.result) {
@ -175,15 +231,9 @@ export class GraysProductBid extends ProductBid {
await delay(500); await delay(500);
const price_value = (await page.$eval('#priceValue', (el) => el.value)) || null; const { price_value } = await this.update();
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}`); if (!price_value) return;
this.handleUpdateBid({ lot_id, reserve_price: price_value, close_time, name, current_price: current_price ? extractNumber(current_price) : null });
const { result, bid_price } = await this.validate({ page, price_value }); const { result, bid_price } = await this.validate({ page, price_value });
@ -203,7 +253,7 @@ export class GraysProductBid extends ProductBid {
if (!resultPlaceBid) { if (!resultPlaceBid) {
console.log('❌ Error occurred while placing the bid.'); console.log('❌ Error occurred while placing the bid.');
await takeSnapshot(page, this, 'place-bid-action'); await takeSnapshot(page, this, 'place-bid-action');
await safeClosePage(this); // await safeClosePage(this);
return; return;
} }
@ -212,7 +262,7 @@ export class GraysProductBid extends ProductBid {
await this.handleReturnProductPage(page); await this.handleReturnProductPage(page);
} catch (error) { } catch (error) {
console.error(`🚨 Error navigating the page: ${error.message}`); console.error(`🚨 Error navigating the page: ${error.message}`);
safeClosePage(this); // safeClosePage(this);
} }
}; };
} }

View File

@ -24,6 +24,7 @@ export class ProductBid extends Bid {
current_price; current_price;
name; name;
reserve_price; reserve_price;
update;
constructor({ constructor({
url, url,
@ -133,4 +134,15 @@ export class ProductBid extends Bid {
return true; 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.');
}
} }

View File

@ -5,6 +5,9 @@ import configs from '../system/config.js';
import CONSTANTS from '../system/constants.js'; import CONSTANTS from '../system/constants.js';
import { sanitizeFileName } from '../system/utils.js'; import { sanitizeFileName } from '../system/utils.js';
import * as fs from 'fs'; import * as fs from 'fs';
const ONE_MINUTE = 60 * 1000;
export const handleCloseRemoveProduct = (data) => { export const handleCloseRemoveProduct = (data) => {
if (!Array.isArray(data)) return; if (!Array.isArray(data)) return;
@ -41,3 +44,9 @@ export const deleteProfile = (data) => {
return false; return false;
}; };
export const shouldUpdateProductTab = (productTab) => {
const updatedAt = new Date(productTab.updated_at).getTime();
const now = Date.now();
return now - updatedAt >= ONE_MINUTE;
};

View File

@ -84,7 +84,7 @@ export const updateStatusByPrice = async (id, current_price) => {
method: 'POST', method: 'POST',
url: 'bids/update-status/' + id, url: 'bids/update-status/' + id,
data: { data: {
current_price, current_price: Number(current_price) | 0,
}, },
}); });

View File

@ -17,7 +17,7 @@ export const createOutBidLog = async (values) => {
return data.data; return data.data;
} catch (error) { } catch (error) {
console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error); console.log('❌ ERROR IN SERVER (OUT BID LOG): ', error.message);
return false; return false;
} }
}; };

View File

@ -7,6 +7,7 @@ puppeteer.use(StealthPlugin());
const browser = await puppeteer.launch({ const browser = await puppeteer.launch({
headless: process.env.ENVIRONMENT === 'prod' ? true : false, headless: process.env.ENVIRONMENT === 'prod' ? true : false,
// userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile // userDataDir: CONSTANTS.PROFILE_PATH, // Thư mục lưu profile
timeout: 60000,
args: [ args: [
'--no-sandbox', '--no-sandbox',
'--disable-setuid-sandbox', '--disable-setuid-sandbox',

View File

@ -6,7 +6,7 @@ import { updateStatusWork } from './apis/bid.js';
export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value)); export const isNumber = (value) => !isNaN(value) && !isNaN(parseFloat(value));
export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => { export const takeSnapshot = async (page, item, imageName, type = CONSTANTS.TYPE_IMAGE.ERRORS) => {
if (page.isClosed()) return; if (!page || page.isClosed()) return;
try { try {
const baseDir = path.join(CONSTANTS.ERROR_IMAGES_PATH, item.type, String(item.id)); // Thư mục theo lot_id 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}`); 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 // Chụp ảnh màn hình và lưu vào filePath
await page.screenshot({ path: filePath, fullPage: true }); await page.screenshot({ path: filePath, fullPage: true });
@ -79,3 +91,12 @@ export const sanitizeFileName = (url) => {
export const getPathProfile = (origin_url) => { export const getPathProfile = (origin_url) => {
return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json'); return path.join(CONSTANTS.PROFILE_PATH, sanitizeFileName(origin_url) + '.json');
}; };
export function removeFalsyValues(obj, excludeKeys = []) {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value || excludeKeys.includes(key)) {
acc[key] = value;
}
return acc;
}, {});
}