Files
x-poster-client/src/x-poster/x-cookie.service.ts
T
2026-05-11 03:18:19 +00:00

244 lines
10 KiB
TypeScript

// 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<string>('TWITTER_AUTH_TOKEN');
// const ct0 = this.config.get<string>('TWITTER_CT0')!;
this.queryID = this.config.get<string>('TWITTER_QUERY_ID') || '';
const createQid = this.config.get<string>('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<any> {
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;
}
}
}