From c8ccb032d268ef335b756078b652f3151ae46075 Mon Sep 17 00:00:00 2001 From: NAME Date: Tue, 12 May 2026 02:30:05 +0000 Subject: [PATCH] update --- src/sqs-module/sqs.poster.worker.ts | 4 +- src/x-poster/utils/x-headers.util.ts | 39 +++++ src/x-poster/x-browser.service.ts | 17 ++- src/x-poster/x-poster.router.service.ts | 31 +--- src/xbot-follow/playwright-x.service.ts | 195 +++++++++++++----------- src/xbot-follow/xbot-follow.module.ts | 2 - src/xbot-follow/xbot-follow.service.ts | 48 +++--- 7 files changed, 194 insertions(+), 142 deletions(-) diff --git a/src/sqs-module/sqs.poster.worker.ts b/src/sqs-module/sqs.poster.worker.ts index 4a5e1d6..70fdc39 100644 --- a/src/sqs-module/sqs.poster.worker.ts +++ b/src/sqs-module/sqs.poster.worker.ts @@ -145,7 +145,9 @@ export class SqsPosterWorker { return r } catch (e) { this.logger.error(e); - await this.notifyService.sendMessageToTele(`Worker==> doReplyTweet error:${e.message}`) + console.log("Mã lỗi:", e.code); // Ví dụ: 'ECONNABORTED' (timeout), 'ERR_NETWORK' (mất mạng) + console.log("Thông báo:", e.message); + await this.notifyService.sendMessageToTele(`Worker==> doReplyTweet error:${e.code} - ${e.message}`) } diff --git a/src/x-poster/utils/x-headers.util.ts b/src/x-poster/utils/x-headers.util.ts index 019171f..8592cfa 100644 --- a/src/x-poster/utils/x-headers.util.ts +++ b/src/x-poster/utils/x-headers.util.ts @@ -32,3 +32,42 @@ export function buildXCookies() { {name: "lang", value: "en"}, ]; } + +export function getAccount() { + return { + id: process.env.X_USERNAME!, + api: { + accessToken: '', + accessSecret: '', + appKey: process.env.TWITTER_CLIENT_ID + '', + appSecret: process.env.TWITTER_CLIENT_SECRET!, + }, + cookie: { + authToken: process.env.X_COOKIE_AUTH_TOKEN!, // auth_token cookie + ct0: process.env.X_COOKIE_CT0!, // ct0 cookie (CSRF token) + kdt: process.env.X_COOKIE_KDT!, // ct0 cookie (CSRF token) + proxy: '', + }, + browser: { + accountId: process.env.X_USERNAME!, + cookies: [ + { + name: 'auth_token', + value: process.env.X_COOKIE_AUTH_TOKEN!, + }, + { + name: 'ct0', + value: process.env.X_COOKIE_CT0!, + }, + { + name: 'kdt', + value: process.env.X_COOKIE_KDT!, + }, + ], + proxy: '', + userAgent: process.env.BROWSER_USER_AGENT || '', + headless: Number(process.env.BROWSER_IS_HEADLESS) === 1, + }, + }; + +} diff --git a/src/x-poster/x-browser.service.ts b/src/x-poster/x-browser.service.ts index 199ba9f..0ee2356 100644 --- a/src/x-poster/x-browser.service.ts +++ b/src/x-poster/x-browser.service.ts @@ -2,6 +2,7 @@ import {HttpException, Injectable, Logger, OnModuleDestroy, OnModuleInit,} from '@nestjs/common'; import {Browser, BrowserContext, chromium, Page} from 'playwright'; import {rand} from "../helper"; +import {getAccount} from "./utils/x-headers.util"; export interface BrowserAccount { accountId: string; @@ -13,6 +14,7 @@ export interface BrowserAccount { }>; proxy?: string; userAgent?: string; + headless?: boolean; } export interface BrowserTweetResult { @@ -38,13 +40,15 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { setInterval(() => this.cleanupStaleContexts(), 60_000); } - private async ensureBrowser(): Promise { + private async ensureBrowser(headless = true): Promise { if (this.browser && this.browser.isConnected()) return this.browser; this.logger.log('Launching Chromium...'); this.browser = await chromium.launch({ - headless: true, + headless, args: [ '--disable-blink-features=AutomationControlled', + '--disable-features=IsolateOrigins,site-per-process', + '--disable-infobars', '--no-sandbox', '--disable-dev-shm-usage', ], @@ -76,7 +80,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { } console.log('getOrCreateContext:3') - const browser = await this.ensureBrowser(); + const browser = await this.ensureBrowser(account.headless); console.log('getOrCreateContext:4') const ctx = await browser.newContext({ @@ -115,7 +119,12 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { return ctx; } - private async getPage(account: BrowserAccount): Promise { + async newPage(): Promise { + const account = getAccount(); + return this.getPage(account.browser) + } + + async getPage(account: BrowserAccount): Promise { let ctx = await this.getOrCreateContext(account); console.log('Đã khởi tạo ctx') if (ctx.isClosed()) { diff --git a/src/x-poster/x-poster.router.service.ts b/src/x-poster/x-poster.router.service.ts index 91bff0e..e781792 100644 --- a/src/x-poster/x-poster.router.service.ts +++ b/src/x-poster/x-poster.router.service.ts @@ -5,6 +5,7 @@ import {BrowserAccount, XBrowserService} from "./x-browser.service"; import {XApiService} from "./x-api.service"; import {XCookieService} from "./x-cookie.service"; import {NotifyService} from "../notify.service"; +import {getAccount} from "./utils/x-headers.util"; export enum SUPPORT_SOCIAL_PROVIDERS { FB = 'fb', @@ -51,35 +52,7 @@ export class XPosterRouterService { private readonly browserSvc: XBrowserService, private readonly notifyService: NotifyService, ) { - this.X_UNIFIED_ACCOUNT = { - id: process.env.X_USERNAME!, - api: { - accessToken: '', - accessSecret: '', - appKey: process.env.TWITTER_CLIENT_ID + '', - appSecret: process.env.TWITTER_CLIENT_SECRET!, - }, - cookie: { - authToken: process.env.X_COOKIE_AUTH_TOKEN!, // auth_token cookie - ct0: process.env.X_COOKIE_CT0!, // ct0 cookie (CSRF token) - proxy: '', - }, - browser: { - accountId: process.env.X_USERNAME!, - cookies: [ - { - name: 'auth_token', - value: process.env.X_COOKIE_AUTH_TOKEN!, - }, - { - name: 'ct0', - value: process.env.X_COOKIE_CT0!, - }, - ], - proxy: '', - userAgent: process.env.BROWSER_USER_AGENT || '', - }, - }; + this.X_UNIFIED_ACCOUNT = getAccount(); console.error(this.X_UNIFIED_ACCOUNT); diff --git a/src/xbot-follow/playwright-x.service.ts b/src/xbot-follow/playwright-x.service.ts index 5bbe1df..8f8b37b 100644 --- a/src/xbot-follow/playwright-x.service.ts +++ b/src/xbot-follow/playwright-x.service.ts @@ -1,88 +1,107 @@ -import {Injectable, Logger, OnModuleDestroy, OnModuleInit} from '@nestjs/common'; -import {ConfigService} from '@nestjs/config'; -import {Browser, BrowserContext, chromium, Page} from 'playwright'; - -@Injectable() -export class PlaywrightXService implements OnModuleInit, OnModuleDestroy { - private readonly logger = new Logger(PlaywrightXService.name); - private browser: Browser; - private context: BrowserContext; - - constructor(private readonly config: ConfigService) { - } - - async onModuleInit() { - // Launch với stealth args để giảm bị detect - this.browser = await chromium.launch({ - headless: false, // Để true khi đã test ổn định - args: [ - '--disable-blink-features=AutomationControlled', - '--disable-features=IsolateOrigins,site-per-process', - '--disable-infobars', - '--no-sandbox', - '--window-size=1366,768', - ], - }); - - this.context = await this.browser.newContext({ - viewport: {width: 1366, height: 768}, - userAgent: process.env.BROWSER_USER_AGENT || - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + - '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - locale: process.env.BROWSER_LOCALE || 'en-US', - permissions: ['notifications'], - }); - - // Pre-warm: set cookie rồi vào X kiểm tra login - await this.restoreSession(); - } - - async onModuleDestroy() { - await this.context?.close(); - await this.browser?.close(); - } - - /** Tạo page mới từ context đã login */ - async newPage(): Promise { - return this.context.newPage(); - } - - /** Set cookie auth_token, ct0, kdt vào context */ - private async restoreSession() { - const authToken = this.config.get('X_COOKIE_AUTH_TOKEN'); - const ct0 = this.config.get('X_COOKIE_CT0'); - const kdt = this.config.get('X_COOKIE_KDT') || ''; - - if (!authToken || !ct0) { - this.logger.warn('🚨 Thiếu TWITTER_AUTH_TOKEN hoặc CT0 trong .env'); - return; - } - - await this.context.addCookies([ - { - name: 'auth_token', - value: authToken, - domain: '.x.com', - path: '/', - httpOnly: true, - secure: true, - sameSite: 'None' - }, - {name: 'ct0', value: ct0, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'}, - {name: 'kdt', value: kdt, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'}, - ]); - - const page = await this.context.newPage(); - await page.goto('https://x.com/home', {waitUntil: 'domcontentloaded', timeout: 30000}); - await page.waitForTimeout(3000); - - const isLoggedIn = await page - .locator('[data-testid="SideNav_AccountSwitcher_Button"], [data-testid="AppTabBar_Home_Link"]') - .first() - .isVisible() - .catch(() => false); - - this.logger.log(`🔐 Session restore: ${isLoggedIn ? 'LOGGED IN' : 'GUEST (cookie có thể expired)'}`); - await page.close(); - } -} \ No newline at end of file +// import {Injectable, Logger, OnModuleDestroy, OnModuleInit} from '@nestjs/common'; +// import {ConfigService} from '@nestjs/config'; +// import {Browser, BrowserContext, chromium, Page} from 'playwright'; +// +// @Injectable() +// export class PlaywrightXService implements OnModuleInit, OnModuleDestroy { +// private readonly logger = new Logger(PlaywrightXService.name); +// private browser: Browser; +// private context: BrowserContext; +// +// constructor(private readonly config: ConfigService) { +// } +// +// async onModuleInit() { +// // Launch với stealth args để giảm bị detect +// this.browser = await chromium.launch({ +// headless: false, // Để true khi đã test ổn định +// args: [ +// '--disable-blink-features=AutomationControlled', +// '--disable-features=IsolateOrigins,site-per-process', +// '--disable-infobars', +// '--no-sandbox', +// '--window-size=1366,768', +// ], +// }); +// +// this.context = await this.browser.newContext({ +// viewport: {width: 1366, height: 768}, +// userAgent: process.env.BROWSER_USER_AGENT || +// 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + +// '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', +// locale: process.env.BROWSER_LOCALE || 'en-US', +// permissions: ['notifications'], +// }); +// +// // Pre-warm: set cookie rồi vào X kiểm tra login +// await this.restoreSession(); +// } +// +// async onModuleDestroy() { +// await this.context?.close(); +// await this.browser?.close(); +// } +// +// // async onModuleInit() { +// // // Lazy launch – chỉ mở khi cần +// // setInterval(() => this.cleanupStaleContexts(), 60_000); +// // } +// +// private async ensureBrowser(): Promise { +// if (this.browser && this.browser.isConnected()) return this.browser; +// this.logger.log('Launching Chromium...'); +// this.browser = await chromium.launch({ +// headless: true, +// args: [ +// '--disable-blink-features=AutomationControlled', +// '--no-sandbox', +// '--disable-dev-shm-usage', +// ], +// }); +// return this.browser; +// } +// +// /** Tạo page mới từ context đã login */ +// async newPage(): Promise { +// return this.context.newPage(); +// } +// +// /** Set cookie auth_token, ct0, kdt vào context */ +// private async restoreSession() { +// const authToken = this.config.get('X_COOKIE_AUTH_TOKEN'); +// const ct0 = this.config.get('X_COOKIE_CT0'); +// const kdt = this.config.get('X_COOKIE_KDT') || ''; +// +// if (!authToken || !ct0) { +// this.logger.warn('🚨 Thiếu TWITTER_AUTH_TOKEN hoặc CT0 trong .env'); +// return; +// } +// +// await this.context.addCookies([ +// { +// name: 'auth_token', +// value: authToken, +// domain: '.x.com', +// path: '/', +// httpOnly: true, +// secure: true, +// sameSite: 'None' +// }, +// {name: 'ct0', value: ct0, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'}, +// {name: 'kdt', value: kdt, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'}, +// ]); +// +// const page = await this.context.newPage(); +// await page.goto('https://x.com/home', {waitUntil: 'domcontentloaded', timeout: 30000}); +// await page.waitForTimeout(3000); +// +// const isLoggedIn = await page +// .locator('[data-testid="SideNav_AccountSwitcher_Button"], [data-testid="AppTabBar_Home_Link"]') +// .first() +// .isVisible() +// .catch(() => false); +// +// this.logger.log(`🔐 Session restore: ${isLoggedIn ? 'LOGGED IN' : 'GUEST (cookie có thể expired)'}`); +// await page.close(); +// } +// } \ No newline at end of file diff --git a/src/xbot-follow/xbot-follow.module.ts b/src/xbot-follow/xbot-follow.module.ts index b5b6cb6..14a0916 100644 --- a/src/xbot-follow/xbot-follow.module.ts +++ b/src/xbot-follow/xbot-follow.module.ts @@ -1,13 +1,11 @@ import {Global, Module} from "@nestjs/common"; import {XbotFollowController} from "./xbot-follow.controller"; import {XbotFollowService} from "./xbot-follow.service"; -import {PlaywrightXService} from "./playwright-x.service"; @Global() @Module({ imports: [], providers: [ - PlaywrightXService, XbotFollowService ], controllers: [XbotFollowController], diff --git a/src/xbot-follow/xbot-follow.service.ts b/src/xbot-follow/xbot-follow.service.ts index 1946b66..01ef04f 100644 --- a/src/xbot-follow/xbot-follow.service.ts +++ b/src/xbot-follow/xbot-follow.service.ts @@ -1,7 +1,8 @@ import {Injectable, Logger} from '@nestjs/common'; import {Page} from 'playwright'; -import {PlaywrightXService} from './playwright-x.service'; import {FollowBatchResult, FollowFollowersOptions, FollowOneResult} from './types'; +import {XBrowserService} from "../x-poster/x-browser.service"; +import {rand} from "../helper"; @Injectable() export class XbotFollowService { @@ -9,9 +10,10 @@ export class XbotFollowService { private readonly sessionFollowed = new Set(); // cache tránh follow lại trong cùng phiên constructor( - private readonly pwService: PlaywrightXService, + private readonly pwService: XBrowserService, // private readonly config: ConfigService, - ) {} + ) { + } // =================== 1. FOLLOW TRỰC TIẾP =================== @@ -27,22 +29,29 @@ export class XbotFollowService { waitUntil: 'domcontentloaded', timeout: 30000, }); - await p.waitForLoadState('networkidle'); + await p.waitForTimeout(1000 + (Math.random() + Math.random()) * 3000); + await p.mouse.wheel(0, rand(300, 500)); + await p.waitForTimeout(rand(1000, 3000)); + await this.humanDelay(1500, 3000); + this.logger.log(`➡️ Check account tồn tại...`); // Check account tồn tại if (await this.isTextPresent(p, /doesn\'t exist|Coundn.t be found/)) { - return { username: target, success: false, alreadyFollowing: false, error: 'Account not found' }; + return {username: target, success: false, alreadyFollowing: false, error: 'Account not found'}; } + this.logger.log(`➡️ Nếu đã follow rồi -> nút sẽ là "Following" (unfollowButton)`); // Nếu đã follow rồi -> nút sẽ là "Following" (unfollowButton) - if (await this.isVisible(p, 'button[data-testid="unfollowButton"]')) { + if (await this.isVisible(p, 'button[data-testid$="-unfollow"]')) { this.logger.log(` → Already following @${target}`); - return { username: target, success: true, alreadyFollowing: true }; + return {username: target, success: true, alreadyFollowing: true}; } - // Click Follow - const followBtn = p.locator('button[data-testid="followButton"]:not([disabled])').first(); + this.logger.log(`➡️Click follow`); + + // Click Follow: data-testid kết thúc bằng "-follow" + const followBtn = p.locator('button[data-testid$="-follow"]').first(); if (await followBtn.isVisible().catch(() => false)) { await followBtn.scrollIntoViewIfNeeded(); await this.humanDelay(400, 900); @@ -50,8 +59,8 @@ export class XbotFollowService { await this.humanDelay(1200, 2500); - // Verify chuyển sang Following - const success = await this.isVisible(p, 'button[data-testid="unfollowButton"]'); + // Verify chuyển sang unfollow + const success = await this.isVisible(p, 'button[data-testid$="-unfollow"]'); if (success) this.sessionFollowed.add(target); return { @@ -62,10 +71,13 @@ export class XbotFollowService { }; } - return { username: target, success: false, alreadyFollowing: false, error: 'Follow button not found' }; + + this.logger.error(`❌Follow button not found`); + + return {username: target, success: false, alreadyFollowing: false, error: 'Follow button not found'}; } catch (err: any) { - this.logger.error(` ❌ Lỗi follow @${target}: ${err.message}`); - return { username: target, success: false, alreadyFollowing: false, error: err.message }; + this.logger.error(`❌ Lỗi follow @${target}: ${err.message}`); + return {username: target, success: false, alreadyFollowing: false, error: err.message}; } finally { if (shouldClose) await p.close(); } @@ -77,7 +89,7 @@ export class XbotFollowService { targetUsername: string, options: FollowFollowersOptions = {}, ): Promise { - const { limit = 5, delayRange = [2000, 5000] } = options; + const {limit = 5, delayRange = [2000, 5000]} = options; const source = targetUsername.replace(/^@/, '').toLowerCase(); const result: FollowBatchResult = { @@ -117,7 +129,7 @@ export class XbotFollowService { const user = href.replace('/', '').split('?')[0].toLowerCase(); if (!user || user === source) continue; // Bỏ qua chính chủ if (this.sessionFollowed.has(user)) { - result.skipped.push({ username: user, reason: 'already-in-session' }); + result.skipped.push({username: user, reason: 'already-in-session'}); continue; } @@ -128,7 +140,7 @@ export class XbotFollowService { .catch(() => false); if (isAlreadyFollowing) { - result.skipped.push({ username: user, reason: 'already-following' }); + result.skipped.push({username: user, reason: 'already-following'}); continue; } @@ -151,7 +163,7 @@ export class XbotFollowService { this.sessionFollowed.add(user); this.logger.log(` ✅ ${result.followed.length}/${limit} | @${user}`); } else { - result.failed.push({ username: user, error: 'State unchanged after click' }); + result.failed.push({username: user, error: 'State unchanged after click'}); } }