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
} 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}`)
}
+39
View File
@@ -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,
},
};
}
+13 -4
View File
@@ -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<Browser> {
private async ensureBrowser(headless = true): Promise<Browser> {
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<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);
console.log('Đã khởi tạo ctx')
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 {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);
+107 -88
View File
@@ -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<Page> {
return this.context.newPage();
}
/** Set cookie auth_token, ct0, kdt vào context */
private async restoreSession() {
const authToken = this.config.get<string>('X_COOKIE_AUTH_TOKEN');
const ct0 = this.config.get<string>('X_COOKIE_CT0');
const kdt = this.config.get<string>('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();
}
}
// 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<Browser> {
// 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<Page> {
// return this.context.newPage();
// }
//
// /** Set cookie auth_token, ct0, kdt vào context */
// private async restoreSession() {
// const authToken = this.config.get<string>('X_COOKIE_AUTH_TOKEN');
// const ct0 = this.config.get<string>('X_COOKIE_CT0');
// const kdt = this.config.get<string>('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();
// }
// }
-2
View File
@@ -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],
+30 -18
View File
@@ -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<string>(); // 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<FollowBatchResult> {
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'});
}
}