update
This commit is contained in:
parent
fff5447ce1
commit
b1e34be9ce
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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={<IconHistory size={14} />}
|
||||
leftSection={<IconHammer size={14} />}
|
||||
>
|
||||
Bids
|
||||
</Menu.Item>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</Title>
|
||||
|
||||
<Box className="grid grid-cols-4 gap-4">
|
||||
{workingData.map((item, index) => (
|
||||
<WorkingPage socket={socket} data={item} key={item.id + index} />
|
||||
))}
|
||||
{workingData.length > 0 && workingData.map((item, index) => <WorkingPage socket={socket} data={item} key={item.id + index} />)}
|
||||
|
||||
{workingData.length <= 0 && (
|
||||
<Box className="flex items-center justify-center col-span-4">
|
||||
<Text>No Pages</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,3 +61,8 @@ export interface IPermission extends ITimestamp {
|
|||
name: string;
|
||||
description: string;
|
||||
}
|
||||
export interface ISendMessageHistory extends ITimestamp {
|
||||
id: number;
|
||||
message: string;
|
||||
bid: IBid;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
const text = this.formatBidMessage(bid);
|
||||
async sendBidInfo(bid: Bid): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<Bid>,
|
||||
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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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...');
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}, {});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue