xdl-node
Version:
A library for retrieving audio streams and other data from X Spaces, built on Node.js and TypeScript.
307 lines (306 loc) • 12.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TwitterAPI = exports.LiveVideoStreamAPI = exports.FleetsAPI = exports.GraphQLAPI = exports.APIClient = exports.HTTPClient = void 0;
// api.ts
const axios_1 = __importDefault(require("axios"));
const axios_retry_1 = __importDefault(require("axios-retry"));
const cookies_1 = require("./cookies");
// Twitter unofficial API authorization header.
const TWITTER_AUTHORIZATION = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs=1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
// Default connection timeout in milliseconds.
const TIMEOUT = 20000;
/**
* HTTPClient – класс для выполнения GET-запросов с помощью axios с поддержкой retry-логики.
*/
class HTTPClient {
constructor() {
this.axiosInstance = axios_1.default.create({
timeout: TIMEOUT
});
// Добавляем retry‑логику, аналогичную Python‑версии:
(0, axios_retry_1.default)(this.axiosInstance, {
retries: 5,
retryDelay: axios_retry_1.default.exponentialDelay,
retryCondition: (error) => {
return axios_retry_1.default.isNetworkOrIdempotentRequestError(error) ||
!!(error.response && [500, 502, 503, 504].includes(error.response.status));
}
});
}
/**
* Выполняет HTTP GET-запрос по указанному URL.
* @param url URL запроса.
* @param params Параметры запроса.
* @param headers Заголовки запроса.
* @param cookies Cookies для запроса.
* @returns Промис с AxiosResponse.
*/
async get(url, params = {}, headers = {}, cookies = {}) {
try {
// Формируем заголовок Cookie из объекта cookies
const cookieHeader = Object.entries(cookies)
.map(([k, v]) => `${k}=${v}`)
.join('; ');
const response = await this.axiosInstance.get(url, {
params,
headers: { ...headers, Cookie: cookieHeader }
});
return response;
}
catch (error) {
throw new Error(`HTTP GET request failed for ${url}: ${error.message}`);
}
}
}
exports.HTTPClient = HTTPClient;
/**
* APIClient – базовый класс для всех API.
*/
class APIClient {
/**
* @param client Экземпляр HTTPClient.
* @param path Путь, добавляемый к базовому URL.
* @param cookies Cookies для запросов.
*/
constructor(client, path, cookies) {
(0, cookies_1.validateCookies)(cookies);
this.client = client;
// Собираем базовый URL: "https://x.com/i/api/<path>"
this.baseUrl = this.joinUrl("https://x.com/i/api", path);
this.cookies = cookies;
this.headers = {
"authorization": TWITTER_AUTHORIZATION,
"x-csrf-token": cookies.ct0
};
}
/**
* Объединяет несколько частей URL.
* @param paths Части URL.
* @returns Собранный URL.
*/
joinUrl(...paths) {
return paths.map(p => p.replace(/^\/+|\/+$/g, '')).join('/');
}
/**
* Выполняет GET-запрос к API по указанному пути.
* @param path Путь запроса.
* @param params Параметры запроса.
* @returns Распарсенный JSON-ответ.
*/
async get(path, params = {}) {
const url = this.joinUrl(this.baseUrl, path);
try {
const response = await this.client.get(url, params, this.headers, this.cookies);
return response.data;
}
catch (err) {
throw new Error(`API GET request failed for ${url}: ${err.message}`);
}
}
}
exports.APIClient = APIClient;
/**
* GraphQLAPI – клиент для Twitter GraphQL API.
*/
class GraphQLAPI extends APIClient {
constructor(client, path, cookies) {
super(client, path, cookies);
}
_dumpJson(obj) {
return typeof obj === 'string' ? obj : JSON.stringify(obj);
}
/**
* Отправляет GraphQL-запрос.
* @param query_id Идентификатор запроса.
* @param operation_name Имя операции.
* @param variables Переменные запроса.
* @param features Дополнительные флаги (опционально).
* @returns Ответ API.
*/
async query(query_id, operation_name, variables, features = null) {
const params = { variables: this._dumpJson(variables) };
if (features) {
params.features = this._dumpJson(features);
}
const path = this.joinUrl(query_id, operation_name);
return await super.get(path, params);
}
/**
* Запрашивает детали Twitter Space по его ID.
* @param space_id ID Space.
* @returns Ответ API.
*/
async audioSpaceById(space_id) {
const query_id = "xVEzTKg_mLTHubK5ayL0HA";
const operation_name = "AudioSpaceById";
const variables = {
id: space_id,
isMetatagsQuery: true,
withReplays: true,
withListeners: true
};
const features = {
spaces_2022_h2_clipping: true,
spaces_2022_h2_spaces_communities: true,
responsive_web_graphql_exclude_directive_enabled: true,
verified_phone_label_enabled: false,
creator_subscriptions_tweet_preview_api_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
tweetypie_unmention_optimization_enabled: true,
responsive_web_edit_tweet_api_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
view_counts_everywhere_api_enabled: true,
longform_notetweets_consumption_enabled: true,
responsive_web_twitter_article_tweet_consumption_enabled: false,
tweet_awards_web_tipping_enabled: false,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
responsive_web_graphql_timeline_navigation_enabled: true,
longform_notetweets_rich_text_read_enabled: true,
longform_notetweets_inline_media_enabled: true,
responsive_web_media_download_video_enabled: false,
responsive_web_enhance_cards_enabled: false
};
return await this.query(query_id, operation_name, variables, features);
}
/**
* Запрашивает детали пользователя по его screen name.
* @param screen_name Имя пользователя.
* @returns Ответ API.
*/
async userByScreenName(screen_name) {
const query_id = "oUZZZ8Oddwxs8Cd3iW3UEA";
const operation_name = "UserByScreenName";
const variables = { screen_name, withSafetyModeUserFields: true };
const features = {
hidden_profile_likes_enabled: false,
responsive_web_graphql_exclude_directive_enabled: true,
verified_phone_label_enabled: false,
subscriptions_verification_info_verified_since_enabled: true,
highlights_tweets_tab_ui_enabled: true,
creator_subscriptions_tweet_preview_api_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
responsive_web_graphql_timeline_navigation_enabled: true
};
return await this.query(query_id, operation_name, variables, features);
}
/**
* Backup endpoint для запроса деталей пользователя по screen name.
* @param screen_name Имя пользователя.
* @returns Ответ API.
*/
async profileSpotlightsQuery(screen_name) {
const query_id = "ZQEuHPrIYlvh1NAyIQHP_w";
const operation_name = "ProfileSpotlightsQuery";
const variables = { screen_name };
return await this.query(query_id, operation_name, variables);
}
/**
* Получает numeric user ID (rest_id) по screen name.
* @param screen_name Имя пользователя.
* @returns Numeric user ID.
*/
async userId(screen_name) {
try {
const data = await this.userByScreenName(screen_name);
return data.data.user.result.rest_id;
}
catch (err) {
console.warn("Trying with backup endpoint");
const data = await this.profileSpotlightsQuery(screen_name);
return data.data.user_result_by_screen_name.result.rest_id;
}
}
/**
* Получает numeric user ID из URL профиля Twitter.
* @param user_url URL профиля.
* @returns Numeric user ID.
*/
async userIdFromUrl(user_url) {
const regex = /^(?:https?:\/\/)?twitter\.com\/(?<screen_name>\w+)\/?$/;
const match = user_url.trim().match(regex);
if (match && match.groups && match.groups.screen_name) {
return await this.userId(match.groups.screen_name);
}
throw new Error(`Invalid Twitter user URL: ${user_url}`);
}
}
exports.GraphQLAPI = GraphQLAPI;
/**
* FleetsAPI – клиент для Twitter Fleets API.
*/
class FleetsAPI extends APIClient {
constructor(client, path, cookies) {
super(client, path, cookies);
}
/**
* Выполняет GET-запрос к указанному endpoint Fleets API.
* @param version Версия API.
* @param endpoint Endpoint API.
* @param params Параметры запроса.
* @returns Ответ API.
*/
async fetch(version, endpoint, params) {
const urlPath = this.joinUrl(version, endpoint);
return await super.get(urlPath, params);
}
/**
* Запрашивает информацию о аватаре для указанных user_ids.
* @param user_ids Список user_ids.
* @returns Ответ API.
*/
async avatarContent(...user_ids) {
if (user_ids.length > 100) {
throw new Error("Number of user IDs exceeded the limit of 100 per request");
}
const version = "v1";
const endpoint = "avatar_content";
const params = { user_ids: user_ids.join(','), only_spaces: "true" };
return await this.fetch(version, endpoint, params);
}
}
exports.FleetsAPI = FleetsAPI;
/**
* LiveVideoStreamAPI – клиент для Twitter Live Video Stream API.
*/
class LiveVideoStreamAPI extends APIClient {
constructor(client, path, cookies) {
super(client, path, cookies);
}
/**
* Запрашивает детали плейлиста медиа по media_key.
* @param media_key Ключ медиа.
* @returns Ответ API.
*/
async status(media_key) {
const urlPath = this.joinUrl("status", media_key);
return await super.get(urlPath);
}
}
exports.LiveVideoStreamAPI = LiveVideoStreamAPI;
/**
* TwitterAPI – коллекция всех Twitter API.
*/
class TwitterAPI {
constructor() {
this.client = new HTTPClient();
// Изначально API не инициализированы
this.graphql_api = null;
this.fleets_api = null;
this.live_video_stream_api = null;
}
/**
* Инициализирует все API с использованием cookies.
* @param cookies Объект cookies.
*/
initApis(cookies) {
this.graphql_api = new GraphQLAPI(this.client, "graphql", cookies);
this.fleets_api = new FleetsAPI(this.client, "fleets", cookies);
this.live_video_stream_api = new LiveVideoStreamAPI(this.client, "1.1/live_video_stream", cookies);
}
}
exports.TwitterAPI = TwitterAPI;