This commit is contained in:
NAME
2026-05-12 02:30:05 +00:00
parent b5cf2502be
commit c8ccb032d2
7 changed files with 194 additions and 142 deletions
+3 -1
View File
@@ -145,7 +145,9 @@ export class SqsPosterWorker {
return r return r
} catch (e) { } catch (e) {
this.logger.error(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}`)
} }
+39
View File
@@ -32,3 +32,42 @@ export function buildXCookies() {
{name: "lang", value: "en"}, {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,
},
};
}
+13 -4
View File
@@ -2,6 +2,7 @@
import {HttpException, Injectable, Logger, OnModuleDestroy, OnModuleInit,} from '@nestjs/common'; import {HttpException, Injectable, Logger, OnModuleDestroy, OnModuleInit,} from '@nestjs/common';
import {Browser, BrowserContext, chromium, Page} from 'playwright'; import {Browser, BrowserContext, chromium, Page} from 'playwright';
import {rand} from "../helper"; import {rand} from "../helper";
import {getAccount} from "./utils/x-headers.util";
export interface BrowserAccount { export interface BrowserAccount {
accountId: string; accountId: string;
@@ -13,6 +14,7 @@ export interface BrowserAccount {
}>; }>;
proxy?: string; proxy?: string;
userAgent?: string; userAgent?: string;
headless?: boolean;
} }
export interface BrowserTweetResult { export interface BrowserTweetResult {
@@ -38,13 +40,15 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
setInterval(() => this.cleanupStaleContexts(), 60_000); setInterval(() => this.cleanupStaleContexts(), 60_000);
} }
private async ensureBrowser(): Promise<Browser> { private async ensureBrowser(headless = true): Promise<Browser> {
if (this.browser && this.browser.isConnected()) return this.browser; if (this.browser && this.browser.isConnected()) return this.browser;
this.logger.log('Launching Chromium...'); this.logger.log('Launching Chromium...');
this.browser = await chromium.launch({ this.browser = await chromium.launch({
headless: true, headless,
args: [ args: [
'--disable-blink-features=AutomationControlled', '--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
'--disable-infobars',
'--no-sandbox', '--no-sandbox',
'--disable-dev-shm-usage', '--disable-dev-shm-usage',
], ],
@@ -76,7 +80,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
} }
console.log('getOrCreateContext:3') console.log('getOrCreateContext:3')
const browser = await this.ensureBrowser(); const browser = await this.ensureBrowser(account.headless);
console.log('getOrCreateContext:4') console.log('getOrCreateContext:4')
const ctx = await browser.newContext({ const ctx = await browser.newContext({
@@ -115,7 +119,12 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
return ctx; return ctx;
} }
private async getPage(account: BrowserAccount): Promise<Page> { async newPage(): Promise<Page> {
const account = getAccount();
return this.getPage(account.browser)
}
async getPage(account: BrowserAccount): Promise<Page> {
let ctx = await this.getOrCreateContext(account); let ctx = await this.getOrCreateContext(account);
console.log('Đã khởi tạo ctx') console.log('Đã khởi tạo ctx')
if (ctx.isClosed()) { if (ctx.isClosed()) {
+2 -29
View File
@@ -5,6 +5,7 @@ import {BrowserAccount, XBrowserService} from "./x-browser.service";
import {XApiService} from "./x-api.service"; import {XApiService} from "./x-api.service";
import {XCookieService} from "./x-cookie.service"; import {XCookieService} from "./x-cookie.service";
import {NotifyService} from "../notify.service"; import {NotifyService} from "../notify.service";
import {getAccount} from "./utils/x-headers.util";
export enum SUPPORT_SOCIAL_PROVIDERS { export enum SUPPORT_SOCIAL_PROVIDERS {
FB = 'fb', FB = 'fb',
@@ -51,35 +52,7 @@ export class XPosterRouterService {
private readonly browserSvc: XBrowserService, private readonly browserSvc: XBrowserService,
private readonly notifyService: NotifyService, private readonly notifyService: NotifyService,
) { ) {
this.X_UNIFIED_ACCOUNT = { this.X_UNIFIED_ACCOUNT = getAccount();
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 || '',
},
};
console.error(this.X_UNIFIED_ACCOUNT); console.error(this.X_UNIFIED_ACCOUNT);
+107 -88
View File
@@ -1,88 +1,107 @@
import {Injectable, Logger, OnModuleDestroy, OnModuleInit} from '@nestjs/common'; // import {Injectable, Logger, OnModuleDestroy, OnModuleInit} from '@nestjs/common';
import {ConfigService} from '@nestjs/config'; // import {ConfigService} from '@nestjs/config';
import {Browser, BrowserContext, chromium, Page} from 'playwright'; // import {Browser, BrowserContext, chromium, Page} from 'playwright';
//
@Injectable() // @Injectable()
export class PlaywrightXService implements OnModuleInit, OnModuleDestroy { // export class PlaywrightXService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PlaywrightXService.name); // private readonly logger = new Logger(PlaywrightXService.name);
private browser: Browser; // private browser: Browser;
private context: BrowserContext; // private context: BrowserContext;
//
constructor(private readonly config: ConfigService) { // constructor(private readonly config: ConfigService) {
} // }
//
async onModuleInit() { // async onModuleInit() {
// Launch với stealth args để giảm bị detect // // Launch với stealth args để giảm bị detect
this.browser = await chromium.launch({ // this.browser = await chromium.launch({
headless: false, // Để true khi đã test ổn định // headless: false, // Để true khi đã test ổn định
args: [ // args: [
'--disable-blink-features=AutomationControlled', // '--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process', // '--disable-features=IsolateOrigins,site-per-process',
'--disable-infobars', // '--disable-infobars',
'--no-sandbox', // '--no-sandbox',
'--window-size=1366,768', // '--window-size=1366,768',
], // ],
}); // });
//
this.context = await this.browser.newContext({ // this.context = await this.browser.newContext({
viewport: {width: 1366, height: 768}, // viewport: {width: 1366, height: 768},
userAgent: process.env.BROWSER_USER_AGENT || // userAgent: process.env.BROWSER_USER_AGENT ||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + // 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', // '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
locale: process.env.BROWSER_LOCALE || 'en-US', // locale: process.env.BROWSER_LOCALE || 'en-US',
permissions: ['notifications'], // permissions: ['notifications'],
}); // });
//
// Pre-warm: set cookie rồi vào X kiểm tra login // // Pre-warm: set cookie rồi vào X kiểm tra login
await this.restoreSession(); // await this.restoreSession();
} // }
//
async onModuleDestroy() { // async onModuleDestroy() {
await this.context?.close(); // await this.context?.close();
await this.browser?.close(); // await this.browser?.close();
} // }
//
/** Tạo page mới từ context đã login */ // // async onModuleInit() {
async newPage(): Promise<Page> { // // // Lazy launch chỉ mở khi cần
return this.context.newPage(); // // setInterval(() => this.cleanupStaleContexts(), 60_000);
} // // }
//
/** Set cookie auth_token, ct0, kdt vào context */ // private async ensureBrowser(): Promise<Browser> {
private async restoreSession() { // if (this.browser && this.browser.isConnected()) return this.browser;
const authToken = this.config.get<string>('X_COOKIE_AUTH_TOKEN'); // this.logger.log('Launching Chromium...');
const ct0 = this.config.get<string>('X_COOKIE_CT0'); // this.browser = await chromium.launch({
const kdt = this.config.get<string>('X_COOKIE_KDT') || ''; // headless: true,
// args: [
if (!authToken || !ct0) { // '--disable-blink-features=AutomationControlled',
this.logger.warn('🚨 Thiếu TWITTER_AUTH_TOKEN hoặc CT0 trong .env'); // '--no-sandbox',
return; // '--disable-dev-shm-usage',
} // ],
// });
await this.context.addCookies([ // return this.browser;
{ // }
name: 'auth_token', //
value: authToken, // /** Tạo page mới từ context đã login */
domain: '.x.com', // async newPage(): Promise<Page> {
path: '/', // return this.context.newPage();
httpOnly: true, // }
secure: true, //
sameSite: 'None' // /** Set cookie auth_token, ct0, kdt vào context */
}, // private async restoreSession() {
{name: 'ct0', value: ct0, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'}, // const authToken = this.config.get<string>('X_COOKIE_AUTH_TOKEN');
{name: 'kdt', value: kdt, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'}, // const ct0 = this.config.get<string>('X_COOKIE_CT0');
]); // const kdt = this.config.get<string>('X_COOKIE_KDT') || '';
//
const page = await this.context.newPage(); // if (!authToken || !ct0) {
await page.goto('https://x.com/home', {waitUntil: 'domcontentloaded', timeout: 30000}); // this.logger.warn('🚨 Thiếu TWITTER_AUTH_TOKEN hoặc CT0 trong .env');
await page.waitForTimeout(3000); // return;
// }
const isLoggedIn = await page //
.locator('[data-testid="SideNav_AccountSwitcher_Button"], [data-testid="AppTabBar_Home_Link"]') // await this.context.addCookies([
.first() // {
.isVisible() // name: 'auth_token',
.catch(() => false); // value: authToken,
// domain: '.x.com',
this.logger.log(`🔐 Session restore: ${isLoggedIn ? 'LOGGED IN' : 'GUEST (cookie có thể expired)'}`); // path: '/',
await page.close(); // 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();
// }
// }
-2
View File
@@ -1,13 +1,11 @@
import {Global, Module} from "@nestjs/common"; import {Global, Module} from "@nestjs/common";
import {XbotFollowController} from "./xbot-follow.controller"; import {XbotFollowController} from "./xbot-follow.controller";
import {XbotFollowService} from "./xbot-follow.service"; import {XbotFollowService} from "./xbot-follow.service";
import {PlaywrightXService} from "./playwright-x.service";
@Global() @Global()
@Module({ @Module({
imports: [], imports: [],
providers: [ providers: [
PlaywrightXService,
XbotFollowService XbotFollowService
], ],
controllers: [XbotFollowController], controllers: [XbotFollowController],
+21 -9
View File
@@ -1,7 +1,8 @@
import {Injectable, Logger} from '@nestjs/common'; import {Injectable, Logger} from '@nestjs/common';
import {Page} from 'playwright'; import {Page} from 'playwright';
import {PlaywrightXService} from './playwright-x.service';
import {FollowBatchResult, FollowFollowersOptions, FollowOneResult} from './types'; import {FollowBatchResult, FollowFollowersOptions, FollowOneResult} from './types';
import {XBrowserService} from "../x-poster/x-browser.service";
import {rand} from "../helper";
@Injectable() @Injectable()
export class XbotFollowService { export class XbotFollowService {
@@ -9,9 +10,10 @@ export class XbotFollowService {
private readonly sessionFollowed = new Set<string>(); // cache tránh follow lại trong cùng phiên private readonly sessionFollowed = new Set<string>(); // cache tránh follow lại trong cùng phiên
constructor( constructor(
private readonly pwService: PlaywrightXService, private readonly pwService: XBrowserService,
// private readonly config: ConfigService, // private readonly config: ConfigService,
) {} ) {
}
// =================== 1. FOLLOW TRỰC TIẾP =================== // =================== 1. FOLLOW TRỰC TIẾP ===================
@@ -27,22 +29,29 @@ export class XbotFollowService {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
timeout: 30000, 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); await this.humanDelay(1500, 3000);
this.logger.log(`➡️ Check account tồn tại...`);
// Check account tồn tại // Check account tồn tại
if (await this.isTextPresent(p, /doesn\'t exist|Coundn.t be found/)) { 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) // 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}`); this.logger.log(` → Already following @${target}`);
return {username: target, success: true, alreadyFollowing: true}; return {username: target, success: true, alreadyFollowing: true};
} }
// Click Follow this.logger.log(`➡️Click follow`);
const followBtn = p.locator('button[data-testid="followButton"]:not([disabled])').first();
// 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)) { if (await followBtn.isVisible().catch(() => false)) {
await followBtn.scrollIntoViewIfNeeded(); await followBtn.scrollIntoViewIfNeeded();
await this.humanDelay(400, 900); await this.humanDelay(400, 900);
@@ -50,8 +59,8 @@ export class XbotFollowService {
await this.humanDelay(1200, 2500); await this.humanDelay(1200, 2500);
// Verify chuyển sang Following // Verify chuyển sang unfollow
const success = await this.isVisible(p, 'button[data-testid="unfollowButton"]'); const success = await this.isVisible(p, 'button[data-testid$="-unfollow"]');
if (success) this.sessionFollowed.add(target); if (success) this.sessionFollowed.add(target);
return { return {
@@ -62,6 +71,9 @@ export class XbotFollowService {
}; };
} }
this.logger.error(`❌Follow button not found`);
return {username: target, success: false, alreadyFollowing: false, error: 'Follow button not found'}; return {username: target, success: false, alreadyFollowing: false, error: 'Follow button not found'};
} catch (err: any) { } catch (err: any) {
this.logger.error(`❌ Lỗi follow @${target}: ${err.message}`); this.logger.error(`❌ Lỗi follow @${target}: ${err.message}`);