Files
x-poster-client/src/x-poster/x-poster.router.service.ts
T
2026-05-12 03:43:19 +00:00

389 lines
13 KiB
TypeScript

// 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<any> {
return this.cookieSvc.verifyCookie(account, 'UserByScreenName')
}
async postTweet(params: {
text: string;
strategy?: XStrategy;
}): Promise<RouterResult> {
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<RouterResult> {
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<RouterResult> {
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, Array<'api' | 'cookie' | 'browser'>> = {
[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));
}
}