import * as fs from "fs"; import * as path from "path"; import { PuppeteerScreenRecorder } from "puppeteer-screen-recorder"; import { outBid, uploadRecord } from "../system/apis/bid.js"; import BID_TYPE from "../system/bid-type.js"; import browser from "../system/browser.js"; import CONSTANTS from "../system/constants.js"; import { getPathProfile, randomDelayWithMeta } from "../system/utils.js"; import { Bid } from "./bid.js"; import { unlink } from "fs/promises"; export class ProductBid extends Bid { // value 'live' | 'sandbox' static MODE_KEY = "mode_key"; // value is minutes esg: arrival_offset_seconds of web bid parent static ARRIAVAL_OFFSET_SECONDS_LIVE = "arrival_offset_seconds_live"; static ARRIAVAL_OFFSET_SECONDS_SANDBOX = "arrival_offset_seconds_sandbox"; // value is minutes esg: early_tracking_seconds of web bid parent static EARLY_TRACKING_SECONDS = "early_tracking_seconds"; static EARLY_TRACKING_SECONDS_LIVE = `${this.EARLY_TRACKING_SECONDS}_live`; static EARLY_TRACKING_SECONDS_SANDBOX = `${this.EARLY_TRACKING_SECONDS}_sandbox`; id; max_price; model; lot_id; plus_price; close_time; first_bid; quantity; created_at; updated_at; histories; start_bid_time; parent_browser_context; web_bid; current_price; name; reserve_price; update; metadata; recorder; name_record; constructor({ url, max_price, plus_price, model, first_bid = false, id, created_at, updated_at, quantity = 1, histories = [], close_time, lot_id, start_bid_time, web_bid, current_price, reserve_price, name, metadata, }) { super(BID_TYPE.PRODUCT_TAB, url); this.max_price = max_price || 0; this.model = model; this.plus_price = plus_price || 0; this.first_bid = first_bid; this.id = id; this.created_at = created_at; this.updated_at = updated_at; this.quantity = quantity; this.histories = histories; this.close_time = close_time; this.lot_id = lot_id; this.start_bid_time = start_bid_time; this.web_bid = web_bid; this.current_price = current_price; this.name = name; this.reserve_price = reserve_price; this.metadata = metadata; } setNewData({ url, max_price, plus_price, model, first_bid = false, id, created_at, updated_at, quantity = 1, histories = [], close_time, lot_id, start_bid_time, web_bid, current_price, reserve_price, name, metadata, }) { this.max_price = max_price || 0; this.model = model; this.plus_price = plus_price || 0; this.first_bid = first_bid; this.id = id; this.created_at = created_at; this.updated_at = updated_at; this.quantity = quantity; this.histories = histories; this.close_time = close_time; this.lot_id = lot_id; this.start_bid_time = start_bid_time; this.web_bid = web_bid; this.url = url; this.current_price = current_price; this.name = name; this.reserve_price = reserve_price; this.metadata = metadata; } puppeteer_connect = async () => { if (!this.parent_browser_context) { console.log( `❌ Connect fail. parent_browser_context is null: ${this.id}` ); return; } const context = await browser.createBrowserContext(); const statusInit = await this.restoreContext(context); if (!statusInit) { console.log(`⚠️ Restore failed.`); return; } const page = await context.newPage(); this.page_context = page; this.browser_context = context; }; async restoreContext(context) { const filePath = getPathProfile(this.web_bid.origin_url); if (!fs.existsSync(filePath)) return false; const contextData = JSON.parse(fs.readFileSync(filePath, "utf8")); // Restore Cookies await context.setCookie(...contextData.cookies); return true; } async gotoLink() { const page = this.page_context; if (page.isClosed()) { console.error("❌ Page has been closed, cannot navigate."); return; } console.log("🔄 Starting the bidding process..."); try { await page.goto(this.url, { waitUntil: "networkidle2" }); console.log(`✅ Navigated to: ${this.url}`); await page.bringToFront(); await page.setUserAgent( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ); console.log("👀 Brought the tab to the foreground."); } catch (error) { console.error("❌ Error during navigation:", error); } } getMode = () => { return ( this.metadata.find((item) => item.key_name === ProductBid.MODE_KEY) ?.value || "live" ); }; getEarlyTrackingSeconds = () => { const mode = this.getMode(); return ( this.metadata.find( (item) => item.key_name === `${ProductBid.EARLY_TRACKING_SECONDS}_${mode}` )?.value || this.web_bid.early_tracking_seconds ); }; isSandbox() { return this.getMode() !== "live"; } async delayForAction() { // Thực thi hành động console.log(`[${this.id}] 🚀 Executing action`); const { wait, delay } = randomDelayWithMeta(); console.log( `[${this.id}] ⏳ Delay for action: ${(delay / 1000).toFixed(2)} seconds` ); await wait; console.log(`[${this.id}] ✅ Finished delay`); } async close() { await outBid(this.id); } async startRecordSandbox() { if ( !this.page_context || !this.name || this.recorder || this.getMode() === "live" ) return; const dirPath = CONSTANTS.RECORD_VIDEO_PATH; // 📁 Kiểm tra và tạo thư mục nếu chưa có if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); // recursive để tạo nested folder nếu cần console.log("📁 Created recording folder:", dirPath); } this.name_record = `${this.id}_${this.name}_${new Date().getTime()}.mp4`; const filePath = path.join(dirPath, this.name_record); this.recorder = new PuppeteerScreenRecorder(this.page_context); await this.recorder.start(filePath); } async stopRecordSandbox() { if (!this.recorder || this.getMode() === "live") return; await this.recorder.stop(); const filePath = path.join(CONSTANTS.RECORD_VIDEO_PATH, this.name_record); const result = await uploadRecord(this, filePath); return result; } ACTION_URL(options = { type: "action" }) { return `${process.env.BASE_URL}bids/hook-action?id=${this.id}&type=${options.type}`; } }