first commit
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user