update
This commit is contained in:
@@ -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}`)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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],
|
||||||
|
|||||||
@@ -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,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) {
|
} catch (err: any) {
|
||||||
this.logger.error(` ❌ Lỗi follow @${target}: ${err.message}`);
|
this.logger.error(`❌ Lỗi follow @${target}: ${err.message}`);
|
||||||
return { username: target, success: false, alreadyFollowing: false, error: err.message };
|
return {username: target, success: false, alreadyFollowing: false, error: err.message};
|
||||||
} finally {
|
} finally {
|
||||||
if (shouldClose) await p.close();
|
if (shouldClose) await p.close();
|
||||||
}
|
}
|
||||||
@@ -77,7 +89,7 @@ export class XbotFollowService {
|
|||||||
targetUsername: string,
|
targetUsername: string,
|
||||||
options: FollowFollowersOptions = {},
|
options: FollowFollowersOptions = {},
|
||||||
): Promise<FollowBatchResult> {
|
): Promise<FollowBatchResult> {
|
||||||
const { limit = 5, delayRange = [2000, 5000] } = options;
|
const {limit = 5, delayRange = [2000, 5000]} = options;
|
||||||
const source = targetUsername.replace(/^@/, '').toLowerCase();
|
const source = targetUsername.replace(/^@/, '').toLowerCase();
|
||||||
|
|
||||||
const result: FollowBatchResult = {
|
const result: FollowBatchResult = {
|
||||||
@@ -117,7 +129,7 @@ export class XbotFollowService {
|
|||||||
const user = href.replace('/', '').split('?')[0].toLowerCase();
|
const user = href.replace('/', '').split('?')[0].toLowerCase();
|
||||||
if (!user || user === source) continue; // Bỏ qua chính chủ
|
if (!user || user === source) continue; // Bỏ qua chính chủ
|
||||||
if (this.sessionFollowed.has(user)) {
|
if (this.sessionFollowed.has(user)) {
|
||||||
result.skipped.push({ username: user, reason: 'already-in-session' });
|
result.skipped.push({username: user, reason: 'already-in-session'});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +140,7 @@ export class XbotFollowService {
|
|||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
|
|
||||||
if (isAlreadyFollowing) {
|
if (isAlreadyFollowing) {
|
||||||
result.skipped.push({ username: user, reason: 'already-following' });
|
result.skipped.push({username: user, reason: 'already-following'});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +163,7 @@ export class XbotFollowService {
|
|||||||
this.sessionFollowed.add(user);
|
this.sessionFollowed.add(user);
|
||||||
this.logger.log(` ✅ ${result.followed.length}/${limit} | @${user}`);
|
this.logger.log(` ✅ ${result.followed.length}/${limit} | @${user}`);
|
||||||
} else {
|
} else {
|
||||||
result.failed.push({ username: user, error: 'State unchanged after click' });
|
result.failed.push({username: user, error: 'State unchanged after click'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user