244 lines
10 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|