// src/modules/x-router/x-router.service.ts import {Injectable, Logger} from '@nestjs/common'; import {XCookieAccount} from "./interfaces/x-cookie.interface"; 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', X = 'x' } export enum XStrategy { COOKIE_FIRST = 'cookie_first', // rẻ nhất → fallback browser → api API_FIRST = 'api_first', // ổn định nhất → fallback cookie → browser BROWSER_FIRST = 'browser_first', // khi cần chống bot nặng COOKIE_ONLY = 'cookie_only', API_ONLY = 'api_only', BROWSER_ONLY = 'browser_only', AUTO = 'auto', // dựa vào health account BROWSER_API = 'browser_api', BROWSER_COOKIE = 'browser_cookie' } export interface UnifiedAccount { id: string; api?: { accessToken: string; accessSecret: string; appKey: string; appSecret: string }; cookie?: XCookieAccount; browser?: BrowserAccount; } export interface RouterResult { success: boolean; tweetId?: string; via: 'api' | 'cookie' | 'browser'; attempts: Array<{ method: string; error?: string }>; error?: string; } @Injectable() export class XPosterRouterService { private readonly logger = new Logger(XPosterRouterService.name); private readonly X_UNIFIED_ACCOUNT: UnifiedAccount; constructor( private readonly apiSvc: XApiService, private readonly cookieSvc: XCookieService, private readonly browserSvc: XBrowserService, private readonly notifyService: NotifyService, ) { this.X_UNIFIED_ACCOUNT = getAccount(); console.error(this.X_UNIFIED_ACCOUNT); } async verifyCookie(account: XCookieAccount): Promise { return this.cookieSvc.verifyCookie(account, 'UserByScreenName') } async postTweet(params: { text: string; strategy?: XStrategy; }): Promise { const account = this.X_UNIFIED_ACCOUNT; const strategy = params.strategy ?? XStrategy.BROWSER_FIRST; const chain = this.buildChain(strategy, account); const attempts: RouterResult['attempts'] = []; for (const method of chain) { this.logger.log(`[${account.id}] Trying via ${method}`); const result = await this.executeTweet(method, account, params.text); attempts.push({method, error: result.error}); 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`); return { success: true, tweetId: result.tweetId, via: method, attempts, }; } else { } // Nếu lỗi không fallback được (vd: text quá dài) → dừng if (this.isFatalError(result.error)) { this.logger.log(`Dăng bài thất bại isFatalError`); return { success: false, via: method, attempts, error: result.error, }; } this.logger.log(`Dăng bài thất bại, thử phương pháp khác`); } return { success: false, via: chain[chain.length - 1], attempts, error: 'All methods failed', }; } async postReply(params: { text: string; tweetUrl: string; tweetId: string; strategy?: XStrategy; }): Promise { const account = this.X_UNIFIED_ACCOUNT; const strategy = params.strategy ?? XStrategy.BROWSER_FIRST; const chain = this.buildChain(strategy, account); const attempts: RouterResult['attempts'] = []; // await this.browserSvc.postReply( // account.browser!, // params.tweetUrl, params.text // ); // return { // success: true, // tweetId: params.tweetUrl, // via: 'browser', // attempts, // }; for (const method of chain) { this.logger.log(`[${account.id}] Trying reply via ${method}`); const result = await this.executeReply(method, account, { text: params.text, tweetUrl: params.tweetUrl, tweetId: params.tweetId }); attempts.push({method, error: result.error}); if (result.success) { return { success: true, tweetId: result.tweetId, via: method, attempts, }; } // Nếu lỗi không fallback được (vd: text quá dài) → dừng if (this.isFatalError(result.error)) { return { success: false, via: method, attempts, error: result.error, }; } } return { success: false, via: chain[chain.length - 1], attempts, error: 'All methods failed', }; } async postQuote(params: { text: string; tweetUrl: string; tweetId: string; strategy?: XStrategy; }): Promise { const account = this.X_UNIFIED_ACCOUNT; const strategy = params.strategy ?? XStrategy.BROWSER_FIRST; const chain = this.buildChain(strategy, account); const attempts: RouterResult['attempts'] = []; // await this.browserSvc.postReply( // account.browser!, // params.tweetUrl, params.text // ); // return { // success: true, // tweetId: params.tweetUrl, // via: 'browser', // attempts, // }; for (const method of chain) { this.logger.log(`[${account.id}] Trying quote via ${method}`); const result = await this.executeQuote(method, account, { text: params.text, tweetUrl: params.tweetUrl, tweetId: params.tweetId, }); attempts.push({ method, error: method + ':' + result.error }); if (result.success) { return { success: true, tweetId: result.tweetId, via: method, attempts, }; } // Nếu lỗi không fallback được (vd: text quá dài) → dừng if (this.isFatalError(result.error)) { return { success: false, via: method, attempts, error: result.error, }; } } return { success: false, via: chain[chain.length - 1], attempts, error: 'All methods failed', }; } /** Xây chain dựa trên strategy + account capabilities */ private buildChain( strategy: XStrategy, account: UnifiedAccount, ): Array<'api' | 'cookie' | 'browser'> { const has = { api: !!account.api, cookie: !!account.cookie, browser: !!account.browser, }; const chains: Record> = { [XStrategy.BROWSER_API]: ['browser', 'api'], [XStrategy.COOKIE_FIRST]: ['cookie', 'browser', 'api'], [XStrategy.API_FIRST]: ['api', 'cookie', 'browser'], [XStrategy.BROWSER_FIRST]: ['browser', 'cookie', 'api'], [XStrategy.COOKIE_ONLY]: ['cookie'], [XStrategy.API_ONLY]: ['api'], [XStrategy.BROWSER_ONLY]: ['browser'], [XStrategy.BROWSER_COOKIE]: ['cookie', 'browser',], [XStrategy.AUTO]: ['cookie', 'browser', 'api'], // có thể dựa health store }; return chains[strategy].filter((m) => has[m]); } private async executeTweet( method: 'api' | 'cookie' | 'browser', account: UnifiedAccount, text: string, ): Promise<{ success: boolean; tweetId?: string; error?: string }> { try { if (method === 'api' && account.api && account.api?.appKey) { const {data: r} = await this.apiSvc.postSimpleTweet(text); return { tweetId: r.id, success: true, } } if (method === 'cookie' && account.cookie) { return await this.cookieSvc.createTweet(account.cookie, text); } if (method === 'browser' && account.browser) { return await this.browserSvc.postTweet(account.browser, text); } return {success: false, error: `Method ${method} not configured`}; } catch (e: any) { return {success: false, error: e.message}; } } private async executeReply( method: 'api' | 'cookie' | 'browser', account: UnifiedAccount, params: { text: string, tweetUrl: string, tweetId: string } ): Promise<{ success: boolean; tweetId?: string; error?: string }> { try { if (method === 'api' && account.api) { this.logger.error(`api not supported`); return { error: 'Api not supported', success: false, } } if (method === 'cookie' && account.cookie) { // const r = await this.cookieSvc.createReplyTweet( // account.cookie!, // params.text, // params.tweetId! // ); // // // this.logger.error(`quote api not supported`); // return { // error: '', // success: true, // } this.logger.error(`cookie not supported`); // return { // success: false, // error: 'Cookie not supported', // } } if (method === 'browser' && account.browser) { return await this.browserSvc.postReply( account.browser, params.tweetUrl, params.text ); } return {success: false, error: `Method ${method} not configured`}; } catch (e: any) { return {success: false, error: e.message}; } } private async executeQuote( method: 'api' | 'cookie' | 'browser', account: UnifiedAccount, params: { text: string, tweetUrl: string, tweetId: string, } ): Promise<{ success: boolean; tweetId?: string; error?: string }> { try { if (method === 'api' && account.api) { this.logger.error(`quote api not supported`); return { success: false, error: 'quote api not supported', } } if (method === 'cookie' && account.cookie) { // return await this.cookieSvc.verifyCookie(account.cookie, params.text); // const r = await this.cookieSvc.createQuoteTweet( // account.cookie!, // params.text, // params.tweetId! // ); this.logger.error(`quote api not supported`); return { error: '', success: true, } } if (method === 'browser' && account.browser) { return await this.browserSvc.postQuote( account.browser, params.tweetUrl!, params.text ); } return {success: false, error: `Method ${method} not configured`}; } catch (e: any) { return {success: false, error: e.message}; } } private isFatalError(error?: string): boolean { if (!error) return false; const fatalPatterns = [ /duplicate/i, /too long/i, /forbidden content/i, /suspended/i, ]; return fatalPatterns.some((p) => p.test(error)); } }