diff --git a/src/sqs-module/sqs.poster.worker.ts b/src/sqs-module/sqs.poster.worker.ts index 6b81f06..430bf48 100644 --- a/src/sqs-module/sqs.poster.worker.ts +++ b/src/sqs-module/sqs.poster.worker.ts @@ -19,7 +19,7 @@ export class SqsPosterWorker { } 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 @@ -31,11 +31,11 @@ export class SqsPosterWorker { 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; } @@ -63,7 +63,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': { @@ -111,16 +111,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`); + this.logger.log(`==> doPostTweet publish to fb`); await this.facebookApi.postToPage(text); 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}); @@ -150,7 +150,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) { @@ -163,8 +163,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}`) @@ -178,7 +178,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) { diff --git a/src/x-poster/x-browser.service.ts b/src/x-poster/x-browser.service.ts index 4f4e1cb..e51e335 100644 --- a/src/x-poster/x-browser.service.ts +++ b/src/x-poster/x-browser.service.ts @@ -60,15 +60,15 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { account: BrowserAccount, useCache = true ): Promise { - 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 +78,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 +92,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 +111,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 +126,9 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { async getPage(account: BrowserAccount): Promise { 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,7 +136,7 @@ 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(); } @@ -178,6 +178,68 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { } } + 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, @@ -228,7 +290,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { .first() .isVisible() .catch(() => false); - console.log(`postTweet: ${isLoggedIn ? 'LOGGED IN' : 'LOGGED OUT'}`); + this.logger.debug(`postTweet: ${isLoggedIn ? 'LOGGED IN' : 'LOGGED OUT'}`); await page.mouse.wheel(200, rand(300, 800)); await page.waitForTimeout(rand(2000, 5000)); @@ -249,7 +311,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); @@ -260,13 +322,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 @@ -296,7 +358,7 @@ 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; } @@ -307,12 +369,12 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { .first() .isVisible() .catch(() => false); - console.log(`postQuote: ${isLoggedIn ? 'LOGGED IN' : 'LOGGED OUT'}`); + this.logger.debug(`postQuote: ${isLoggedIn ? 'LOGGED IN' : 'LOGGED OUT'}`); // ===== 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); @@ -323,15 +385,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); } @@ -341,7 +404,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(); @@ -350,7 +413,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; // } // @@ -367,7 +430,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); } @@ -378,34 +441,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: ''}; @@ -420,15 +483,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) => ({ @@ -446,15 +509,15 @@ 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}); const isLoggedIn = await page @@ -462,20 +525,22 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy { .first() .isVisible() .catch(() => false); - console.log(`postReply: ${isLoggedIn ? 'LOGGED IN' : 'LOGGED OUT'}`); + this.logger.debug(`postReply: ${isLoggedIn ? 'LOGGED IN' : 'LOGGED OUT'}`); // 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) @@ -485,23 +550,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}`); diff --git a/src/x-poster/x-poster.controller.ts b/src/x-poster/x-poster.controller.ts index 3f2018d..28aa938 100644 --- a/src/x-poster/x-poster.controller.ts +++ b/src/x-poster/x-poster.controller.ts @@ -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!, diff --git a/src/x-poster/x-poster.router.service.ts b/src/x-poster/x-poster.router.service.ts index 5ca51ec..6fb16d0 100644 --- a/src/x-poster/x-poster.router.service.ts +++ b/src/x-poster/x-poster.router.service.ts @@ -102,7 +102,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,