first commit
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
// 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";
|
||||
|
||||
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'// khi cần chống bot nặng
|
||||
}
|
||||
|
||||
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 = {
|
||||
id: process.env.X_USERNAME!,
|
||||
api: {
|
||||
accessToken: '',
|
||||
accessSecret: '',
|
||||
appKey: process.env.TWITTER_CLIENT_ID + '',
|
||||
appSecret: process.env.TWITTER_CLIENT_SECRET!,
|
||||
},
|
||||
cookie: {
|
||||
authToken: process.env.X_COOKIE_AUTH_TOKEN!, // auth_token cookie
|
||||
ct0: process.env.X_COOKIE_CT0!, // ct0 cookie (CSRF token)
|
||||
proxy: '',
|
||||
},
|
||||
browser: {
|
||||
accountId: process.env.X_USERNAME!,
|
||||
cookies: [
|
||||
{
|
||||
name: 'auth_token',
|
||||
value: process.env.X_COOKIE_AUTH_TOKEN!,
|
||||
},
|
||||
{
|
||||
name: 'ct0',
|
||||
value: process.env.X_COOKIE_CT0!,
|
||||
},
|
||||
],
|
||||
proxy: '',
|
||||
userAgent: ''
|
||||
},
|
||||
};
|
||||
|
||||
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: 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) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user