413 lines
14 KiB
TypeScript
413 lines
14 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";
|
|
import {XCacheService} from "../x-cache/x-cache.service";
|
|
|
|
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,
|
|
private readonly xCacheService: XCacheService,
|
|
) {
|
|
this.X_UNIFIED_ACCOUNT = getAccount();
|
|
|
|
console.error(this.X_UNIFIED_ACCOUNT);
|
|
|
|
}
|
|
|
|
async verifyCookie(): Promise<any> {
|
|
console.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.xCacheService.setStateXCookiesIsSillALive();
|
|
this.notifyService.sendMessageToTele('✅Verify cookie pass').catch((err) => {})
|
|
}
|
|
|
|
async canUseXCookies(): Promise<boolean> {
|
|
return this.xCacheService.isXCookiesAlive()
|
|
}
|
|
|
|
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'] = [];
|
|
|
|
const canUseCookie = this.canUseXCookies();
|
|
for (const method of chain) {
|
|
this.logger.log(`[${account.id}] Trying via ${method}`);
|
|
|
|
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');
|
|
continue;
|
|
}
|
|
}
|
|
|
|
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(`Đă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));
|
|
}
|
|
}
|