// x-cookie.service.ts import {Injectable, Logger} from '@nestjs/common'; import axios, {AxiosInstance} from 'axios'; import {HttpsProxyAgent} from 'https-proxy-agent'; import {XCookieAccount} from './interfaces/x-cookie.interface'; import {buildXHeaders} from './utils/x-headers.util'; import {ConfigService} from "@nestjs/config"; interface GraphQLVariables { [key: string]: any; } @Injectable() export class XCookieService { private readonly logger = new Logger(XCookieService.name); // private readonly client: AxiosInstance; private userId: string; private queryID: string; private screenName: string | null = null; private TWEET_URL = ''; constructor(private readonly config: ConfigService) { // ── Lấy cookie từ env ── // const authToken = X_COOKIE_NAME.auth_token; // const ct0 = X_COOKIE_NAME.cto; // const kdt = X_COOKIE_NAME.kdt; // const authToken = this.config.get('TWITTER_AUTH_TOKEN'); // const ct0 = this.config.get('TWITTER_CT0')!; this.queryID = this.config.get('TWITTER_QUERY_ID') || ''; const createQid = this.config.get('TWITTER_CREATE_TWEET_QUERY_ID') || 'Qkq4oPdZYuNB_Qw3TDuFqQ'; this.TWEET_URL = `https://x.com/i/api/graphql/${createQid}/CreateTweet`; // Bearer token này gần như cố định cho Web App, có thể để default const bearer = 'AAAAAAAAAAAAAAAAAAAAAF7OAAAAAAAPS6nVJjCEf6gW6rLVnQujDGAh8%3DkQS5VtDfPBTSO89WMK4HvSpSUYshWVio9dNNWQEvwfkGmL7nPF'; console.log(`userId=${this.userId}`); } private buildClient(account: XCookieAccount): AxiosInstance { console.log('buildClient'); const config: any = { // baseURL: 'https://x.com/i/api', headers: buildXHeaders(account.authToken, account.ct0), timeout: 20000, 'referer': 'https://x.com/compose/tweet', 'origin': 'https://x.com', }; if (account.proxy) { const agent = new HttpsProxyAgent(account.proxy); config.httpsAgent = agent; config.proxy = false; } const client = axios.create(config); // Interceptor: log lỗi chi tiết nếu bị 403/404/400 client.interceptors.response.use( (res) => res, (err) => { const status = err.response?.status; const errors = err.response?.data?.errors; this.logger.log(`Call Url: ${err.config?.url}`) this.logger.error(`Twitter HTTP ${status}:`, errors || err.message); return Promise.reject(err); }, ); return client; } /** * ĐĂNG TWEET đơn */ async createTweet(account: XCookieAccount, text: string): Promise { const payload = { variables: { tweet_text: text, dark_request: false, media: {media_entities: [], possibly_sensitive: false}, semantic_annotation_id: [], }, features: { tweets_nudges_moments: true, tweet_with_visibility_results_prefetch_gql_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, interactive_text_enabled: true, responsive_web_text_conversations_enabled: false, longform_notetweets_richtext_consumption_enabled: false, responsive_web_enhance_cards_enabled: false, }, }; const client = this.buildClient(account); const res = await client.post(this.TWEET_URL, payload); const result = res.data?.data?.create_tweet?.tweet_results?.result; if (!result) { throw new Error(`CreateTweet failed: ${JSON.stringify(res.data)}`); } const tid = result.rest_id; const screenName = result.core?.user_results?.result?.legacy?.screen_name || 'i'; return { id: tid, url: `https://x.com/${screenName}/status/${tid}`, success: true }; } /** ============================================ * 2. REPLY TWEET * ============================================ */ async createReplyTweet(account: XCookieAccount, text: string, replyToTweetId: string): Promise<{ id: string; url: string }> { const payload = { variables: { tweet_text: text, reply: { in_reply_to_tweet_id: replyToTweetId, exclude_reply_user_ids: [], }, dark_request: false, media: {media_entities: [], possibly_sensitive: false}, }, features: { tweets_nudges_moments: true, tweet_with_visibility_results_prefetch_gql_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, interactive_text_enabled: true, responsive_web_text_conversations_enabled: false, longform_notetweets_richtext_consumption_enabled: false, responsive_web_enhance_cards_enabled: false, }, }; const client = this.buildClient(account); const res = await client.post(this.TWEET_URL, payload); const result = res.data?.data?.create_tweet?.tweet_results?.result; if (!result) throw new Error('Reply tweet failed'); return {id: result.rest_id, url: `https://x.com/i/web/status/${result.rest_id}`}; } /** ============================================ * 3. QUOTE TWEET (Retweet kèm cmt) * ============================================ */ async createQuoteTweet(account: XCookieAccount, content: string, quoteTweetId: string): Promise<{ id: string; url: string }> { // Cách 1: Dùng attachment_url (X Web thường dùng cách này) const payload = { variables: { tweet_text: content, dark_request: false, media: {media_entities: [], possibly_sensitive: false}, attachment_url: `https://x.com/i/web/status/${quoteTweetId}`, semantic_annotation_id: [], }, features: this.getCommonFeatures(), }; const client = this.buildClient(account); const res = await client.post(this.TWEET_URL, payload); return this.extractTweetResult(res.data); } private getCommonFeatures() { return { tweets_nudges_moments: true, tweet_with_visibility_results_prefetch_gql_enabled: true, longform_notetweets_consumption_enabled: true, responsive_web_edit_tweet_api_enabled: true, graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, view_counts_everywhere_api_enabled: true, interactive_text_enabled: true, responsive_web_text_conversations_enabled: false, longform_notetweets_richtext_consumption_enabled: false, responsive_web_enhance_cards_enabled: false, }; } private extractTweetResult(data: any): { id: string; url: string } { const result = data?.data?.create_tweet?.tweet_results?.result; if (!result) throw new Error(`CreateTweet failed: ${JSON.stringify(data)}`); const tid = result.rest_id; const screenName = result.core?.user_results?.result?.legacy?.screen_name || 'i'; return {id: tid, url: `https://x.com/${screenName}/status/${tid}`}; } /** * VERIFY COOKIE (FIX lỗi 34): Dùng GraphQL UserByRestId (hash ổn định hơn v1.1) * @param account * @param userHandle Optional: Nếu biết @username */ async verifyCookie(account: XCookieAccount, userHandle?: string): Promise<{ screen_name: string; name: string }> { try { let variables: GraphQLVariables; let queryId: string; // if (userHandle) { // UserByScreenName (hash hiện tại 2026: extract nếu lỗi) queryId = 'IGgvgiOx4QZndDHuD3x9TQ'; // PLACEHOLDER → Extract (xem dưới) variables = { "screen_name": userHandle, "withSafetyModeUserFields": true, "withSuperFollowsUserFields": true }; // } // else { // // UserByRestId (dùng userId từ twid) // queryId = 'IGgvgiOx4QZndDHuD3x9TQ'; // Placeholder → Extract // variables = { "userId": this.userId, "withSafetyModeUserFields": true, "withSuperFollowsUserFields": true }; // } // const account = { // authToken: X_COOKIE_NAME.auth_token, // ct0: X_COOKIE_NAME.cto, // } const client = this.buildClient(account); const res = await client.get( `https://x.com/i/api/graphql/IGgvgiOx4QZndDHuD3x9TQ/UserByScreenName?variables=%7B%22screen_name%22%3A%22elonmusk%22%2C%22withSafetyModeUserFields%22%3Atrue%7D&features=%7B%7D`,) .catch((e)=>{ console.error(e.message); }); // return res.data.data.user; const user = res?.data.data.user; // Hoặc res.data.data.result this.screenName = user.rest_id ? user.legacy.screen_name : user.screen_name; this.logger.log(`Verified: @${this.screenName}`); return {screen_name: this.screenName!, name: ''}; } catch (error: any) { this.logger.error(`Verify failed: ${error.response?.data?.errors?.[0]?.message || error.message}`); throw error; } } }