Compare commits
14 Commits
a3decd63de
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 65768643e0 | |||
| b57c9aa0bd | |||
| 702a0d1ab5 | |||
| 31129e23f6 | |||
| a7ba436169 | |||
| 10d61d59be | |||
| b4be37fbe7 | |||
| d6220c828e | |||
| 1d7bddae27 | |||
| 38ed73d7e6 | |||
| aaf28c1463 | |||
| 62a01a118b | |||
| 8a7df08b6a | |||
| f62a71aede |
@@ -58,3 +58,5 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
dist
|
||||
.idea
|
||||
dist.zip
|
||||
|
||||
data/*
|
||||
@@ -34,6 +34,7 @@
|
||||
"class-validator": "^0.15.1",
|
||||
"cookie": "^1.1.1",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"keyv": "^5.6.0",
|
||||
"lodash": "^4.18.1",
|
||||
"playwright": "^1.59.1",
|
||||
"playwright-extra": "^4.3.6",
|
||||
|
||||
Generated
+943
-1298
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,26 @@
|
||||
import {Controller, Get, Post} from '@nestjs/common';
|
||||
import {AppService} from './app.service';
|
||||
import {XCookieAccountDto} from "./x-poster/dto/x-cookie-account.dto";
|
||||
import {XBrowserService} from "./x-poster/x-browser.service";
|
||||
import {XPosterRouterService} from "./x-poster/x-poster.router.service";
|
||||
import {Context} from "node:vm";
|
||||
import {XCacheService} from "./x-cache/x-cache.service";
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(
|
||||
private readonly appService: AppService,
|
||||
private readonly xBrowserService: XBrowserService,
|
||||
private readonly xPosterRouterService: XPosterRouterService,
|
||||
private readonly cache: XCacheService
|
||||
) {
|
||||
}
|
||||
|
||||
@Get()
|
||||
getHello() {
|
||||
return this.xBrowserService.verifyCookie();
|
||||
return this.xPosterRouterService.verifyCookie();
|
||||
}
|
||||
|
||||
@Post('/set-x-cookies')
|
||||
@Get('/x')
|
||||
setXCookies(dto: XCookieAccountDto) {
|
||||
|
||||
return this.cache.getCacheTwRefreshToken();
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -5,8 +5,12 @@ import {SqsModule} from "./sqs-module/sqs.module";
|
||||
import {XPosterModule} from "./x-poster/x-poster.module";
|
||||
import {ConfigModule} from "@nestjs/config";
|
||||
import {CacheModule} from "@nestjs/cache-manager";
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import {XbotFollowModule} from "./xbot-follow/xbot-follow.module";
|
||||
import * as path from 'path';
|
||||
import {XCacheService} from "./x-cache/x-cache.service";
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
|
||||
console.log(`sqlite://${path.join(process.cwd(), 'cache.sqlite')}`)
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -20,7 +24,7 @@ import {XbotFollowModule} from "./xbot-follow/xbot-follow.module";
|
||||
isGlobal: true,
|
||||
useFactory: () => ({
|
||||
stores: [
|
||||
new KeyvRedis(`redis://127.0.0.1:6379/1`)
|
||||
new KeyvRedis(process.env.REDIS_URL)
|
||||
],
|
||||
}),
|
||||
}),
|
||||
@@ -29,7 +33,7 @@ import {XbotFollowModule} from "./xbot-follow/xbot-follow.module";
|
||||
XbotFollowModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
providers: [AppService, XCacheService,],
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
|
||||
+4
-1
@@ -2,8 +2,12 @@ import {NestFactory} from '@nestjs/core';
|
||||
import {AppModule} from './app.module';
|
||||
import {SqsPosterWorker} from "./sqs-module/sqs.poster.worker";
|
||||
import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger";
|
||||
import fs from 'fs';
|
||||
|
||||
async function bootstrap() {
|
||||
|
||||
fs.mkdirSync('./data', { recursive: true });
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Cấu hình Swagger
|
||||
@@ -28,7 +32,6 @@ async function bootstrap() {
|
||||
`)
|
||||
);
|
||||
|
||||
|
||||
await app.get(SqsPosterWorker).start();
|
||||
|
||||
}
|
||||
|
||||
@@ -8,13 +8,17 @@ export class NotifyService {
|
||||
|
||||
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
|
||||
const CHAT_ID = process.env.TELEGRAM_CHAT_ID!;
|
||||
const X_USERNAME = process.env.X_USERNAME!;
|
||||
|
||||
await axios.post(
|
||||
`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`,
|
||||
{
|
||||
chat_id: CHAT_ID,
|
||||
text: message,
|
||||
text: `F:${X_USERNAME}==>${message}`,
|
||||
parse_mode: 'HTML'
|
||||
},
|
||||
{
|
||||
timeout: 10000 // 5 seconds
|
||||
}
|
||||
);
|
||||
|
||||
@@ -33,6 +37,9 @@ export class NotifyService {
|
||||
chat_id: CHAT_ID,
|
||||
text: `X:${X_USERNAME}==>${message}`,
|
||||
parse_mode: 'HTML'
|
||||
},
|
||||
{
|
||||
timeout: 10000 // 5 seconds
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// sqs.module.ts
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { SqsService } from './sqs.service';
|
||||
import {Global, Module} from '@nestjs/common';
|
||||
import {SqsService} from './sqs.service';
|
||||
import {SqsPostService} from "./sqs.post.service";
|
||||
import {SqsPosterWorker} from "./sqs.poster.worker";
|
||||
import {XPosterRouterService} from "../x-poster/x-poster.router.service";
|
||||
import {XPosterModule} from "../x-poster/x-poster.module";
|
||||
import {FacebookApi} from "../x-poster/facebook.api";
|
||||
import {NotifyService} from "../notify.service";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -17,6 +17,7 @@ import {NotifyService} from "../notify.service";
|
||||
SqsPosterWorker,
|
||||
FacebookApi,
|
||||
NotifyService,
|
||||
XCacheService,
|
||||
],
|
||||
exports: [SqsService],
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import {SUPPORT_SOCIAL_PROVIDERS, XPosterRouterService, XStrategy} from "../x-po
|
||||
import {rand} from "../helper";
|
||||
import {FacebookApi} from "../x-poster/facebook.api";
|
||||
import {NotifyService} from "../notify.service";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
|
||||
@Injectable()
|
||||
export class SqsPosterWorker {
|
||||
@@ -15,24 +16,30 @@ export class SqsPosterWorker {
|
||||
private readonly xRouterService: XPosterRouterService,
|
||||
private readonly facebookApi: FacebookApi,
|
||||
private readonly notifyService: NotifyService,
|
||||
private readonly xCacheService: XCacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.log(`🚀 Worker started for ${await this.sqs.getQueueName()}`);
|
||||
this.logger.log(`🚀 Worker started for ${await this.sqs.getQueueName()}`);
|
||||
await this.notifyService.sendMessageToTele(`🚀 Worker started for ${await this.sqs.getQueueName()}`)
|
||||
|
||||
//check cookie
|
||||
this.xRouterService.verifyCookie();
|
||||
this.xCacheService.isXCookiesAlive().then(isLive => {
|
||||
this.logger.log(`cache cookie is ${isLive ? 'LIVE' : 'DIE'}`);
|
||||
if (!isLive) {
|
||||
this.xRouterService.verifyCookie();
|
||||
}
|
||||
});
|
||||
|
||||
let ReceiptHandle = '';
|
||||
while (true) {
|
||||
try {
|
||||
console.log('worker get message ...');
|
||||
this.logger.log('worker get message ...');
|
||||
const msg = await this.sqs.getMessage();
|
||||
|
||||
if (!msg) {
|
||||
console.log('no message , sleeping...');
|
||||
this.logger.log('no message , sleeping...');
|
||||
await this.sleep(10000); //sleep 10s
|
||||
continue;
|
||||
}
|
||||
@@ -60,7 +67,7 @@ export class SqsPosterWorker {
|
||||
}
|
||||
|
||||
private async process(data: any) {
|
||||
console.log('📩 Got job:', data);
|
||||
this.logger.log('📩 Got job:', data);
|
||||
const {type, content, xSubmitProvider, tweetUrl, publishTo, tweetId, telegramChatId} = data;
|
||||
switch (type) {
|
||||
case 'X_POSTER_TWEET': {
|
||||
@@ -68,6 +75,7 @@ export class SqsPosterWorker {
|
||||
content,
|
||||
publishTo,
|
||||
xSubmitProvider);
|
||||
await this.sleep(10000); //sleep 10s
|
||||
break;
|
||||
}
|
||||
case 'X_POSTER_REPLY': {
|
||||
@@ -108,16 +116,16 @@ export class SqsPosterWorker {
|
||||
strategy: string = XStrategy.API_ONLY,
|
||||
) {
|
||||
try {
|
||||
console.log(`==> doPostTweet`, publishTo);
|
||||
this.logger.log(`==> doPostTweet`, publishTo);
|
||||
let sendSuccess = false;
|
||||
if (publishTo.includes(SUPPORT_SOCIAL_PROVIDERS.FB)) {
|
||||
console.log(`==> doPostTweet publish to fb`);
|
||||
await this.facebookApi.postToPage(text);
|
||||
this.logger.log(`==> doPostTweet publish to fb`);
|
||||
await this.facebookApi.postToPage(text, '', false);
|
||||
await this.notifyService.sendMessageToTele(`Post to FB success`);
|
||||
sendSuccess = true;
|
||||
}
|
||||
if (publishTo.includes(SUPPORT_SOCIAL_PROVIDERS.X)) {
|
||||
console.log(`==> doPostTweet publish to X`);
|
||||
this.logger.log(`==> doPostTweet publish to X`);
|
||||
|
||||
// @ts-ignore
|
||||
const r = await this.xRouterService.postTweet({text, strategy});
|
||||
@@ -147,7 +155,7 @@ export class SqsPosterWorker {
|
||||
strategy: string = XStrategy.BROWSER_COOKIE
|
||||
) {
|
||||
try {
|
||||
console.log('doReplyTweet');
|
||||
this.logger.log('doReplyTweet');
|
||||
// @ts-ignore
|
||||
const r = await this.xRouterService.postReply({text, tweetUrl, tweetId, strategy});
|
||||
if (r.success) {
|
||||
@@ -160,8 +168,8 @@ export class SqsPosterWorker {
|
||||
return r
|
||||
} catch (e) {
|
||||
this.logger.error(e);
|
||||
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);
|
||||
this.logger.log("Mã lỗi:", e.code); // Ví dụ: 'ECONNABORTED' (timeout), 'ERR_NETWORK' (mất mạng)
|
||||
this.logger.log("Thông báo:", e.message);
|
||||
await this.notifyService.sendMessageToTele(`Worker==> doReplyTweet error:${e.code} - ${e.message}`)
|
||||
|
||||
|
||||
@@ -175,7 +183,7 @@ export class SqsPosterWorker {
|
||||
strategy: string = XStrategy.BROWSER_COOKIE
|
||||
) {
|
||||
try {
|
||||
console.log('doQuoteTweet');
|
||||
this.logger.log('doQuoteTweet');
|
||||
// @ts-ignore
|
||||
const r = await this.xRouterService.postQuote({text, tweetUrl, tweetId, strategy});
|
||||
if (r.success) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// src/modules/social/facebook-api.service.ts
|
||||
import {HttpException, HttpStatus, Injectable} from '@nestjs/common';
|
||||
import {HttpException, HttpStatus, Injectable, Logger} from '@nestjs/common';
|
||||
import axios from 'axios';
|
||||
|
||||
@Injectable()
|
||||
export class FacebookApi {
|
||||
private logger = new Logger('FacebookApi');
|
||||
private readonly fbBaseUrl = 'https://graph.facebook.com/v19.0';
|
||||
private readonly pageAccessToken = process.env.FB_PAGE_ACCESS_TOKEN;
|
||||
private readonly pageId = process.env.FB_PAGE_ID;
|
||||
|
||||
async postToPage(content: string, imageUrl?: string) {
|
||||
async postToPage(content: string, imageUrl?: string, throwEx = true) {
|
||||
// console.log('postToPage==>', content, imageUrl);
|
||||
try {
|
||||
let url = `${this.fbBaseUrl}/${this.pageId}/feed`;
|
||||
@@ -25,14 +26,39 @@ export class FacebookApi {
|
||||
|
||||
const response = await axios.post(url, params);
|
||||
//response.data= { id: '1010286162176053_122107818902775551' }
|
||||
return response.data; // Trả về ID bài viết nếu thành công
|
||||
// return response.data; // Trả về ID bài viết nếu thành công
|
||||
return {
|
||||
success: true,
|
||||
postId: response.data,
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Lỗi khi đăng bài lên FB');
|
||||
console.log(error.message);
|
||||
throw new HttpException(
|
||||
error.response?.data || 'Lỗi khi đăng bài lên FB',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
this.logger.error('Lỗi khi đăng bài lên FB');
|
||||
this.logger.error(error.message);
|
||||
// Kiểm tra xem Facebook có trả về response lỗi không
|
||||
let fbErrormessage = error.message;;
|
||||
if (error.response && error.response.data) {
|
||||
const fbError = error.response.data.error;
|
||||
fbErrormessage = fbError.constructor
|
||||
this.logger.error('--- LỖI FACEBOOK API ---');
|
||||
this.logger.error('Message:', fbErrormessage);
|
||||
this.logger.error('Code:', fbError.code);
|
||||
this.logger.error('Subcode:', fbError.error_subcode);
|
||||
this.logger.error('FB Trace ID:', fbError.fbtrace_id);
|
||||
} else {
|
||||
// Lỗi do mạng hoặc cấu hình Axios sai
|
||||
this.logger.error('Lỗi hệ thống/mạng:', error.message);
|
||||
}
|
||||
if (throwEx) {
|
||||
throw new HttpException(
|
||||
fbErrormessage || 'Fb Lỗi khi đăng bài lên FB',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
postId: 0,
|
||||
error: error.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import {HttpException, Injectable, Logger, OnModuleDestroy, OnModuleInit,} from
|
||||
import {Browser, BrowserContext, chromium, Page} from 'playwright';
|
||||
import {rand} from "../helper";
|
||||
import {getAccount} from "./utils/x-headers.util";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
import {NotifyService} from "../notify.service";
|
||||
|
||||
export interface BrowserAccount {
|
||||
accountId: string;
|
||||
@@ -35,6 +37,12 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly MAX_CONTEXTS = 5;
|
||||
private readonly CONTEXT_TTL_MS = 15 * 60 * 1000; // 15 phút
|
||||
|
||||
constructor(
|
||||
private readonly xCacheService: XCacheService,
|
||||
private readonly notifyService: NotifyService,
|
||||
) {
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
// Lazy launch – chỉ mở khi cần
|
||||
setInterval(() => this.cleanupStaleContexts(), 60_000);
|
||||
@@ -60,15 +68,15 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
account: BrowserAccount,
|
||||
useCache = true
|
||||
): Promise<BrowserContext> {
|
||||
console.log('getOrCreateContext:1')
|
||||
// console.log({account});
|
||||
this.logger.debug('getOrCreateContext:1')
|
||||
// this.logger.debug({account});
|
||||
const cached = this.contextPool.get(account.accountId);
|
||||
if (useCache && cached) {
|
||||
console.log('getOrCreateContext:cached');
|
||||
this.logger.debug('getOrCreateContext:cached');
|
||||
cached.lastUsed = Date.now();
|
||||
return cached.ctx;
|
||||
}
|
||||
console.log('getOrCreateContext:2')
|
||||
this.logger.debug('getOrCreateContext:2')
|
||||
|
||||
// LRU eviction
|
||||
if (this.contextPool.size >= this.MAX_CONTEXTS) {
|
||||
@@ -78,10 +86,10 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
await oldest[1].ctx.close().catch(() => null);
|
||||
this.contextPool.delete(oldest[0]);
|
||||
}
|
||||
console.log('getOrCreateContext:3')
|
||||
this.logger.debug('getOrCreateContext:3')
|
||||
|
||||
const browser = await this.ensureBrowser(account.headless);
|
||||
console.log('getOrCreateContext:4')
|
||||
this.logger.debug('getOrCreateContext:4')
|
||||
|
||||
const ctx = await browser.newContext({
|
||||
userAgent:
|
||||
@@ -92,15 +100,15 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
locale: process.env.BROWSER_LOCALE || 'en-US',
|
||||
proxy: account.proxy ? {server: account.proxy} : undefined,
|
||||
});
|
||||
console.log('getOrCreateContext:5')
|
||||
this.logger.debug('getOrCreateContext:5')
|
||||
|
||||
// Anti-detection: ẩn webdriver flag
|
||||
await ctx.addInitScript(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
||||
});
|
||||
console.log('getOrCreateContext:6')
|
||||
this.logger.debug('getOrCreateContext:6')
|
||||
|
||||
// console.log(account.cookies);
|
||||
// this.logger.debug(account.cookies);
|
||||
|
||||
await ctx.addCookies(
|
||||
account.cookies.map((c) => ({
|
||||
@@ -111,9 +119,9 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
|
||||
this.contextPool.set(account.accountId, {ctx, lastUsed: Date.now()});
|
||||
console.log('getOrCreateContext:7')
|
||||
this.logger.debug('getOrCreateContext:7')
|
||||
|
||||
// console.log({
|
||||
// this.logger.debug({
|
||||
// ctx
|
||||
// })
|
||||
return ctx;
|
||||
@@ -126,9 +134,9 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async getPage(account: BrowserAccount): Promise<Page> {
|
||||
let ctx = await this.getOrCreateContext(account);
|
||||
console.log('Đã khởi tạo ctx')
|
||||
this.logger.debug('Đã khởi tạo ctx')
|
||||
if (ctx.isClosed()) {
|
||||
console.log('browser is closeed, reopen');
|
||||
this.logger.debug('browser is closeed, reopen');
|
||||
ctx = await this.getOrCreateContext(account, false);
|
||||
}
|
||||
const cookies = account.cookies.map((c) => ({
|
||||
@@ -136,28 +144,40 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
domain: c.domain || '.x.com',
|
||||
path: c.path || '/',
|
||||
}));
|
||||
// console.log('cookies:', cookies);
|
||||
// this.logger.debug('cookies:', cookies);
|
||||
await ctx.addCookies(cookies);
|
||||
return ctx.newPage();
|
||||
}
|
||||
|
||||
async verifyCookie() {
|
||||
async verifyCookie(sendNotiWhenAlive = false): Promise<boolean> {
|
||||
const page = await this.newPage();
|
||||
await page.goto('https://x.com/', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await page.waitForTimeout(2000 + (Math.random() + Math.random()) * 3000);
|
||||
await page.mouse.wheel(0, rand(300, 500));
|
||||
// Detect login/challenge screen
|
||||
if (page.url().includes('/login') || page.url().includes('/flow')) {
|
||||
this.logger.error('Cookies is die, please get news');
|
||||
|
||||
try {
|
||||
await page.goto('https://x.com/', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await page.waitForTimeout(2000 + (Math.random() + Math.random()) * 3000);
|
||||
await page.mouse.wheel(0, rand(300, 500));
|
||||
return await this.isCookieLive(page, sendNotiWhenAlive);
|
||||
|
||||
} catch (er) {
|
||||
this.logger.error(`Browser verify cookie fail: ${er.message}`);
|
||||
return false;
|
||||
// return {
|
||||
// success: false,
|
||||
// error: 'Redirected to login',
|
||||
// needsRelogin: true,
|
||||
// };
|
||||
} finally {
|
||||
await page?.close().catch(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
async isCookieLive(page: Page, sendNotiWhenAlive = false): Promise<boolean> {
|
||||
this.logger.debug('isCookieLive?');
|
||||
if (page.url().includes('/home')) {
|
||||
this.logger.log('Cookies live');
|
||||
// await this.xCacheService.setStateXCookiesIsSillALive();
|
||||
if (sendNotiWhenAlive) {
|
||||
// await this.notifyService.sendMessageToTele('✅Verify cookie pass')
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const isLoggedIn = await page
|
||||
.locator('[data-testid="SideNav_AccountSwitcher_Button"], [data-testid="AppTabBar_Home_Link"]')
|
||||
@@ -166,10 +186,94 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
.catch(() => false);
|
||||
|
||||
this.logger.log(`🔐 Session restore: ${isLoggedIn ? 'LOGGED IN' : 'GUEST (cookie có thể expired)'}`);
|
||||
await page.close();
|
||||
// if (isLoggedIn) {
|
||||
// await this.xCacheService.setStateXCookiesIsSillALive();
|
||||
// if (sendNotiWhenAlive) {
|
||||
// await this.notifyService.sendMessageToTele('✅Verify cookie pass')
|
||||
// }
|
||||
// } else {
|
||||
// await this.xCacheService.setStateXCookiesIsDie();
|
||||
// await this.notifyService.sendUrgentMessageToTele('❌Cookie đã hết hạn vui lòng cập nhập để sử dụng.')
|
||||
// }
|
||||
return isLoggedIn;
|
||||
}
|
||||
|
||||
// async actVerifyCookie(): Promise<any> {
|
||||
// this.logger.debug('==> actVerifyCookie');
|
||||
// // const isAlive = await this.cookieSvc.verifyCookie();
|
||||
// const isAlive = await this.verifyCookie();
|
||||
// if (!isAlive) {
|
||||
// await this.xCacheService.setStateXCookiesIsDie();
|
||||
// await this.notifyService.sendUrgentMessageToTele('❌Cookie đã hết hạn vui lòng cập nhập để sử dụng.')
|
||||
// }
|
||||
// await this.xCacheService.setStateXCookiesIsSillALive();
|
||||
// await this.notifyService.sendMessageToTele('✅Verify cookie pass')
|
||||
//
|
||||
// return isAlive;
|
||||
// }
|
||||
|
||||
async likeTweet(tweetUrl: string) {
|
||||
let page: Page | null = null;
|
||||
|
||||
try {
|
||||
page = await this.newPage();
|
||||
|
||||
await page.goto(tweetUrl, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await this.actLikeTweet(page);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async actLikeTweet(page: Page, isCloseAfterEnd = false) {
|
||||
|
||||
try {
|
||||
this.logger.debug('actLikeTweet:');
|
||||
await page.waitForTimeout(2000 + (Math.random() + Math.random()) * 3000);
|
||||
|
||||
// 1. Cuộn xuống 1000 pixel
|
||||
await page.mouse.wheel(0, rand(300, 500));
|
||||
await page.waitForTimeout(rand(2000, 4000));
|
||||
await page.mouse.wheel(0, rand(300, 500));
|
||||
this.logger.debug('actLikeTweet:Đã cuộn xuống');
|
||||
|
||||
// Nghỉ 2 giây để quan sát
|
||||
await page.waitForTimeout(rand(2000, 4000));
|
||||
|
||||
// 2. Cuộn ngược lên lại 500 pixel
|
||||
await page.mouse.wheel(0, -1 * 1000);
|
||||
this.logger.debug('actLikeTweet:Đã cuộn lên');
|
||||
await page.waitForTimeout(rand(500, 1500));
|
||||
//like
|
||||
this.logger.debug('actLikeTweet:Bắt đầu nhấn like');
|
||||
|
||||
// Sử dụng selector cụ thể cho bài viết chính (thường có vai trò là article)
|
||||
const mainTweetLike = page
|
||||
.locator('article[data-testid="tweet"]').first()
|
||||
.locator('button[data-testid="like"]');
|
||||
await mainTweetLike.click();
|
||||
|
||||
this.logger.debug('actLikeTweet:Đã like xong');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (e) {
|
||||
this.logger.debug(e);
|
||||
this.logger.error('actLikeTweet: Error:' + e.message);
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (isCloseAfterEnd) {
|
||||
page.close().catch(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async postTweet(
|
||||
account: BrowserAccount,
|
||||
text: string,
|
||||
@@ -214,6 +318,9 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
needsRelogin: true,
|
||||
};
|
||||
}
|
||||
|
||||
await this.isCookieLive(page, false);
|
||||
|
||||
await page.mouse.wheel(200, rand(300, 800));
|
||||
await page.waitForTimeout(rand(2000, 5000));
|
||||
await page.evaluate(() => window.scrollTo({top: 0, behavior: 'smooth'}));
|
||||
@@ -233,7 +340,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
} catch {
|
||||
await textarea.fill(text);
|
||||
}
|
||||
console.log(' Nhập tweet xong ...');
|
||||
this.logger.debug(' Nhập tweet xong ...');
|
||||
await page.waitForTimeout(2000 + (Math.random() + Math.random()) * 3000);
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
@@ -244,13 +351,13 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
// await page.locator('button[data-testid="tweetButtonInline"]').click({ force: true });
|
||||
const btn = page.locator('button[data-testid="tweetButtonInline"]');
|
||||
const btnBox = await btn.boundingBox();
|
||||
console.log(btnBox);
|
||||
console.log('Nhấn Control+Enter ...');
|
||||
this.logger.debug(btnBox);
|
||||
this.logger.debug('Nhấn Control+Enter ...');
|
||||
|
||||
// @ts-ignore
|
||||
// await page.mouse.click(btnBox?.x + btnBox.width / 2, btnBox.y + btnBox.height / 2);
|
||||
await page.keyboard.press('Control+Enter');
|
||||
console.log('Nhấn Control+Enter done ...');
|
||||
this.logger.debug('Nhấn Control+Enter done ...');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Chờ request CreateTweet hoàn tất
|
||||
@@ -280,15 +387,17 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
try {
|
||||
await page.goto(tweetUrl, {waitUntil: 'domcontentloaded', timeout: 30000});
|
||||
} catch (e) {
|
||||
console.log('❌ Load fail');
|
||||
this.logger.debug('❌ Load fail');
|
||||
throw e;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(rand(2000, 4000));
|
||||
|
||||
await this.isCookieLive(page, false);
|
||||
|
||||
// ===== CHECK LOGIN =====
|
||||
if (await page.locator('input[name="text"]').count()) {
|
||||
console.log('❌ Cookie die → bị redirect login');
|
||||
this.logger.error('❌ Cookie die → bị redirect login');
|
||||
|
||||
throw new HttpException('❌ Không thấy nút retweet (tweet private?)', 500);
|
||||
|
||||
@@ -299,15 +408,16 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
await page.waitForTimeout(rand(1000, 5000));
|
||||
await page.mouse.wheel(0, rand(300, 800));
|
||||
await page.waitForTimeout(rand(4000, 8000));
|
||||
await page.evaluate(() => window.scrollTo({top: 0, behavior: 'smooth'}));
|
||||
await page.mouse.wheel(0, -2000);
|
||||
await page.waitForTimeout(rand(1000, 2000));
|
||||
|
||||
await this.actLikeTweet(page);
|
||||
|
||||
// ===== CLICK RETWEET =====
|
||||
let retweetBtn = page.locator('[data-testid="retweet"]');
|
||||
|
||||
if (!(await retweetBtn.count())) {
|
||||
console.log('❌ Không thấy nút retweet (tweet private?)');
|
||||
this.logger.error('❌ Không thấy nút retweet (tweet private?)');
|
||||
throw new HttpException('❌ Không thấy nút retweet (tweet private?)', 500);
|
||||
}
|
||||
|
||||
@@ -317,7 +427,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
try {
|
||||
await page.locator('a[href="/compose/post"]').click({timeout: 2000});
|
||||
} catch {
|
||||
console.log('fallback → click by text');
|
||||
this.logger.debug('fallback → click by text');
|
||||
await page.locator('a[role="menuitem"]')
|
||||
.filter({hasText: /Quote|Trích dẫn/i})
|
||||
.click();
|
||||
@@ -326,7 +436,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
// let quoteBtn = page.locator('[data-testid="retweetWithComment"]');
|
||||
//
|
||||
// if (!(await quoteBtn.count())) {
|
||||
// console.log('❌ Không thấy nút quote');
|
||||
// this.logger.debug('❌ Không thấy nút quote');
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
@@ -343,7 +453,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
// const box = page.locator('div[role="textbox"]:visible').first();
|
||||
|
||||
if (!(await box.count())) {
|
||||
console.log('❌ Không thấy textbox');
|
||||
this.logger.error('❌ Không thấy textbox');
|
||||
throw new HttpException('❌ Không thấy textbox', 500);
|
||||
}
|
||||
|
||||
@@ -354,34 +464,34 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
// await box.scrollIntoViewIfNeeded();
|
||||
|
||||
// focus trước khi gõ
|
||||
console.log('focus trước khi gõ')
|
||||
this.logger.debug('focus trước khi gõ')
|
||||
await box.click({delay: rand(50, 150)});
|
||||
|
||||
|
||||
for (let char of content) {
|
||||
await box.type(char, {delay: rand(50, 120)});
|
||||
}
|
||||
console.log('gõ quote xong ...')
|
||||
this.logger.debug('gõ quote xong ...')
|
||||
await page.waitForTimeout(rand(1000, 2000));
|
||||
|
||||
// ===== POST =====
|
||||
let postBtn = page.locator('[data-testid="tweetButton"]');
|
||||
console.log('count ...')
|
||||
this.logger.debug('count ...')
|
||||
|
||||
if ((await postBtn.count())) {
|
||||
console.log('click nút quote ...')
|
||||
this.logger.debug('click nút quote ...')
|
||||
await postBtn.click({timeout: 7000}).catch(async (e) => {
|
||||
console.log('❌ Nut click khong duoc, thử dùng bàn phím Control+Enter');
|
||||
this.logger.debug('❌ Nut click khong duoc, thử dùng bàn phím Control+Enter');
|
||||
await page.keyboard.press('Control+Enter');
|
||||
});
|
||||
await page.waitForTimeout(rand(4000, 6000));
|
||||
|
||||
console.log('✅ Quoted thành công');
|
||||
this.logger.debug('✅ Quoted thành công');
|
||||
} else {
|
||||
console.log('❌ Không thấy nút post, gọi Ctr + Enter');
|
||||
this.logger.debug('❌ Không thấy nút post, gọi Ctr + Enter');
|
||||
await page.keyboard.press('Control+Enter');
|
||||
await page.waitForTimeout(rand(4000, 6000));
|
||||
console.log('✅ Quoted thành công');
|
||||
this.logger.debug('✅ Quoted thành công');
|
||||
}
|
||||
return {success: true, error: ''};
|
||||
|
||||
@@ -396,15 +506,15 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async postReply(account: BrowserAccount, tweetUrl, content) {
|
||||
if (!content) {
|
||||
console.log(`Nội dung trả lời không có`);
|
||||
this.logger.debug(`Nội dung trả lời không có`);
|
||||
throw new Error('Nội dung trả lời không có');
|
||||
}
|
||||
|
||||
// let ctx = await this.getOrCreateContext(account);
|
||||
//
|
||||
// console.log('ctx', ctx);
|
||||
// this.logger.debug('ctx', ctx);
|
||||
// if (ctx.isClosed()) {
|
||||
// console.log('browser is closeed, reopen');
|
||||
// this.logger.debug('browser is closeed, reopen');
|
||||
// ctx = await this.getOrCreateContext(account, false);
|
||||
// }
|
||||
// const cookies = account.cookies.map((c) => ({
|
||||
@@ -422,29 +532,33 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
// vào tweet
|
||||
// ===== SAFE GOTO =====
|
||||
try {
|
||||
console.log(`Mo trang web tweetUrl`);
|
||||
this.logger.debug(`Mo trang web tweetUrl`);
|
||||
await page.goto(tweetUrl, {waitUntil: 'domcontentloaded', timeout: 30000});
|
||||
} catch (e) {
|
||||
console.log('❌ Load fail');
|
||||
this.logger.debug('❌ Load fail');
|
||||
throw e;
|
||||
}
|
||||
|
||||
// đợi UI ổn
|
||||
console.log(`đợi UI ổn...`)
|
||||
this.logger.debug(`đợi UI ổn...`)
|
||||
await page.waitForSelector('article', {timeout: 7000});
|
||||
|
||||
await this.isCookieLive(page, false);
|
||||
|
||||
// scroll nhẹ
|
||||
console.log(`scroll nhẹ ...`)
|
||||
this.logger.debug(`scroll nhẹ ...`)
|
||||
await page.mouse.wheel(0, 300);
|
||||
await page.waitForTimeout(1000 + Math.random() * 2000);
|
||||
|
||||
await this.actLikeTweet(page);
|
||||
|
||||
// lấy textbox visible
|
||||
const box = page.locator('div[role="textbox"]:visible').first();
|
||||
|
||||
await box.waitFor({state: 'visible', timeout: 7000});
|
||||
|
||||
// focus
|
||||
console.log(`box focus ...`)
|
||||
this.logger.debug(`box focus ...`)
|
||||
await box.click();
|
||||
|
||||
// nhập content (fallback nếu type fail)
|
||||
@@ -454,23 +568,23 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
|
||||
} catch {
|
||||
await box.fill(content);
|
||||
}
|
||||
console.log(`nhập nội dung xong ...`)
|
||||
this.logger.debug(`nhập nội dung xong ...`)
|
||||
await page.waitForTimeout(800 + Math.random() * 1200);
|
||||
|
||||
// nút reply
|
||||
const btn = page.locator('[data-testid="tweetButtonInline"]:visible');
|
||||
|
||||
if (!(await btn.count())) {
|
||||
console.log('❌ Không thấy nút reply');
|
||||
this.logger.debug('❌ Không thấy nút reply');
|
||||
throw new Error('Không thấy nút reply');
|
||||
// return false;
|
||||
}
|
||||
|
||||
await btn.click();
|
||||
console.log(`nhấn nút gửi ...`)
|
||||
this.logger.debug(`nhấn nút gửi ...`)
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
console.log('✅ Reply OK');
|
||||
this.logger.debug('✅ Reply OK');
|
||||
return {success: true, error: ''};
|
||||
} catch (err) {
|
||||
this.logger.error(`Browser reply failed: ${err.message}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// x-poster.controller.ts
|
||||
import {Body, Controller, Get, HttpException, Post, Query, Req} from '@nestjs/common';
|
||||
import {CreateTweetDto, ReplyTweetDto} from './dto/create-tweet.dto';
|
||||
import {XPosterRouterService, XStrategy} from "./x-poster.router.service";
|
||||
import {XPosterRouterService} from "./x-poster.router.service";
|
||||
import {XCookieService} from "./x-cookie.service";
|
||||
import {XApiService} from "./x-api.service";
|
||||
import {XCacheService} from "../x-cache/x-cache.service";
|
||||
@@ -107,8 +107,14 @@ export class XPosterController {
|
||||
) {
|
||||
}
|
||||
|
||||
@Get('verify')
|
||||
verify() {
|
||||
@Get('like')
|
||||
async likeTweet(@Query('xurl') url: string) {
|
||||
console.log('xurl==>', url);
|
||||
if (!url) {
|
||||
throw new HttpException('xUrl not found', 400);
|
||||
}
|
||||
await this.xBrowserService.likeTweet(url);
|
||||
return 'done';
|
||||
// const account = {
|
||||
// authToken: process.env.X_COOKIE_AUTH_TOKEN!, // auth_token cookie
|
||||
// ct0: process.env.X_COOKIE_CT0!,
|
||||
|
||||
@@ -61,13 +61,17 @@ export class XPosterRouterService {
|
||||
}
|
||||
|
||||
async verifyCookie(): Promise<any> {
|
||||
this.logger.debug('==> Verify Cookie');
|
||||
// const isAlive = await this.cookieSvc.verifyCookie();
|
||||
const isAlive = await this.browserSvc.verifyCookie();
|
||||
if (!isAlive) {
|
||||
await this.xCacheService.setStateXCookiesIsDie();
|
||||
await this.notifyService.sendUrgentMessageToTele('Cookie đã hết hạn vui lòng cập nhập để sử dụng.')
|
||||
await this.notifyService.sendUrgentMessageToTele('❌Cookie đã hết hạn vui lòng cập nhập để sử dụng.')
|
||||
}
|
||||
await this.xCacheService.setStateXCookiesIsSillALive();
|
||||
await this.notifyService.sendMessageToTele('✅Verify cookie pass')
|
||||
|
||||
return isAlive;
|
||||
}
|
||||
|
||||
async canUseXCookies(): Promise<boolean> {
|
||||
@@ -89,8 +93,8 @@ export class XPosterRouterService {
|
||||
|
||||
if (['cookie', 'browser'].includes(method)) {
|
||||
if (!canUseCookie) {
|
||||
await this.notifyService.sendUrgentMessageToTele('❌ Vui lòng cập nhập cookie để sử dụng ');
|
||||
this.logger.error('Cookie đã hết hạn, vui lòng cập nhập');
|
||||
await this.notifyService.sendUrgentMessageToTele('❌ Vui lòng cập nhập cookie để sử dụng ');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -100,7 +104,7 @@ export class XPosterRouterService {
|
||||
|
||||
if (result.success) {
|
||||
this.logger.log(`Đã đăng bài thành công`);
|
||||
await this.notifyService.sendMessageToTele(`Đã đăng bài X thành công`);
|
||||
// await this.notifyService.sendMessageToTele(`Đã đăng bài X thành công`);
|
||||
return {
|
||||
success: true,
|
||||
tweetId: result.tweetId,
|
||||
@@ -293,7 +297,8 @@ export class XPosterRouterService {
|
||||
}
|
||||
}
|
||||
if (method === 'cookie' && account.cookie) {
|
||||
return await this.cookieSvc.createTweet(account.cookie, text);
|
||||
return {success: false, error: `Method ${method} not implemented`};
|
||||
// return await this.cookieSvc.createTweet(account.cookie, text);
|
||||
}
|
||||
if (method === 'browser' && account.browser) {
|
||||
return await this.browserSvc.postTweet(account.browser, text);
|
||||
@@ -334,10 +339,10 @@ export class XPosterRouterService {
|
||||
// success: true,
|
||||
// }
|
||||
this.logger.error(`cookie not supported`);
|
||||
// return {
|
||||
// success: false,
|
||||
// error: 'Cookie not supported',
|
||||
// }
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cookie not supported',
|
||||
}
|
||||
}
|
||||
if (method === 'browser' && account.browser) {
|
||||
return await this.browserSvc.postReply(
|
||||
|
||||
Reference in New Issue
Block a user