import { TwitterAuth } from './auth'; import { updateCookieJar } from './requests'; import { AudioSpace, AudioSpaceByIdResponse, AudioSpaceByIdVariables, AuthenticatePeriscopeResponse, BrowseSpaceTopicsResponse, Community, CommunitySelectQueryResponse, LiveVideoStreamStatus, LoginTwitterTokenResponse, Subtopic, } from './types/spaces'; /** * Generates a random string that mimics a UUID v4. */ // TODO: install and replace with uuidv4 function generateRandomId(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } /** * Fetches details of an Audio Space by its ID. * @param variables The variables required for the GraphQL query. * @param auth The authentication object. * @returns The details of the Audio Space. */ export async function fetchAudioSpaceById( variables: AudioSpaceByIdVariables, auth: TwitterAuth, ): Promise { const queryId = 'Tvv_cNXCbtTcgdy1vWYPMw'; // Specific to the AudioSpaceById GraphQL query const operationName = 'AudioSpaceById'; // URL encode the variables and features const variablesEncoded = encodeURIComponent(JSON.stringify(variables)); const features = { spaces_2022_h2_spaces_communities: true, spaces_2022_h2_clipping: true, creator_subscriptions_tweet_preview_api_enabled: true, profile_label_improvements_pcf_label_in_post_enabled: false, rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, premium_content_api_read_enabled: false, communities_web_enable_tweet_community_results_fetch: true, c9s_tweet_anatomy_moderator_badge_enabled: true, responsive_web_grok_analyze_button_fetch_trends_enabled: true, articles_preview_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, 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: true, tweet_awards_web_tipping_enabled: false, creator_subscriptions_quote_tweet_preview_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, rweb_video_timestamps_enabled: true, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_enhance_cards_enabled: false, }; const featuresEncoded = encodeURIComponent(JSON.stringify(features)); const url = `https://x.com/i/api/graphql/${queryId}/${operationName}?variables=${variablesEncoded}&features=${featuresEncoded}`; const onboardingTaskUrl = 'https://api.twitter.com/1.1/onboarding/task.json'; // Retrieve necessary cookies and tokens const cookies = await auth.cookieJar().getCookies(onboardingTaskUrl); const xCsrfToken = cookies.find((cookie) => cookie.key === 'ct0'); const headers = new Headers({ Accept: '*/*', Authorization: `Bearer ${(auth as any).bearerToken}`, 'Content-Type': 'application/json', Cookie: await auth.cookieJar().getCookieString(onboardingTaskUrl), 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'x-guest-token': (auth as any).guestToken, 'x-twitter-auth-type': 'OAuth2Client', 'x-twitter-active-user': 'yes', 'x-csrf-token': xCsrfToken?.value as string, }); const response = await auth.fetch(url, { headers, method: 'GET', }); // Update the cookie jar with any new cookies from the response await updateCookieJar(auth.cookieJar(), response.headers); // Check for errors in the response if (!response.ok) { throw new Error(`Failed to fetch Audio Space: ${await response.text()}`); } const data: AudioSpaceByIdResponse = await response.json(); if (data.errors && data.errors.length > 0) { throw new Error(`API Errors: ${JSON.stringify(data.errors)}`); } return data.data.audioSpace; } /** * Fetches available space topics from Twitter. * @param auth The authentication object. * @returns An array of space topics. */ export async function fetchBrowseSpaceTopics( auth: TwitterAuth, ): Promise { const queryId = 'TYpVV9QioZfViHqEqRZxJA'; const operationName = 'BrowseSpaceTopics'; const variables = {}; const features = {}; const variablesEncoded = encodeURIComponent(JSON.stringify(variables)); const featuresEncoded = encodeURIComponent(JSON.stringify(features)); const url = `https://x.com/i/api/graphql/${queryId}/${operationName}?variables=${variablesEncoded}&features=${featuresEncoded}`; const onboardingTaskUrl = 'https://api.twitter.com/1.1/onboarding/task.json'; // Retrieve necessary cookies and tokens const cookies = await auth.cookieJar().getCookies(onboardingTaskUrl); const xCsrfToken = cookies.find((cookie) => cookie.key === 'ct0'); const headers = new Headers({ Accept: '*/*', Authorization: `Bearer ${(auth as any).bearerToken}`, 'Content-Type': 'application/json', Cookie: await auth.cookieJar().getCookieString(onboardingTaskUrl), 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'x-guest-token': (auth as any).guestToken, 'x-twitter-auth-type': 'OAuth2Client', 'x-twitter-active-user': 'yes', 'x-csrf-token': xCsrfToken?.value as string, }); const response = await auth.fetch(url, { headers, method: 'GET', }); // Update the cookie jar with any new cookies from the response await updateCookieJar(auth.cookieJar(), response.headers); // Check for errors in the response if (!response.ok) { throw new Error(`Failed to fetch Space Topics: ${await response.text()}`); } const data: BrowseSpaceTopicsResponse = await response.json(); if (data.errors && data.errors.length > 0) { throw new Error(`API Errors: ${JSON.stringify(data.errors)}`); } // Flatten the subtopics from all categories into a single array return data.data.browse_space_topics.categories.flatMap( (category) => category.subtopics, ); } /** * Fetches available communities from Twitter. * @param auth The authentication object. * @returns An array of communities. */ export async function fetchCommunitySelectQuery( auth: TwitterAuth, ): Promise { const queryId = 'Lue1DfmoW2cc0225t_8z1w'; // Specific to the CommunitySelectQuery GraphQL query const operationName = 'CommunitySelectQuery'; const variables = {}; const features = {}; const variablesEncoded = encodeURIComponent(JSON.stringify(variables)); const featuresEncoded = encodeURIComponent(JSON.stringify(features)); const url = `https://x.com/i/api/graphql/${queryId}/${operationName}?variables=${variablesEncoded}&features=${featuresEncoded}`; const onboardingTaskUrl = 'https://api.twitter.com/1.1/onboarding/task.json'; // Retrieve necessary cookies and tokens const cookies = await auth.cookieJar().getCookies(onboardingTaskUrl); const xCsrfToken = cookies.find((cookie) => cookie.key === 'ct0'); const headers = new Headers({ Accept: '*/*', Authorization: `Bearer ${(auth as any).bearerToken}`, 'Content-Type': 'application/json', Cookie: await auth.cookieJar().getCookieString(onboardingTaskUrl), 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'x-guest-token': (auth as any).guestToken, 'x-twitter-auth-type': 'OAuth2Client', 'x-twitter-active-user': 'yes', 'x-csrf-token': xCsrfToken?.value as string, }); const response = await auth.fetch(url, { headers, method: 'GET', }); // Update the cookie jar with any new cookies from the response await updateCookieJar(auth.cookieJar(), response.headers); // Check for errors in the response if (!response.ok) { throw new Error( `Failed to fetch Community Select Query: ${await response.text()}`, ); } const data: CommunitySelectQueryResponse = await response.json(); if (data.errors && data.errors.length > 0) { throw new Error(`API Errors: ${JSON.stringify(data.errors)}`); } // Return the space_hostable_communities array, which may be empty return data.data.space_hostable_communities; } /** * Fetches the status of an Audio Space stream by its media key. * @param mediaKey The media key of the Audio Space. * @param auth The authentication object. * @returns The status of the Audio Space stream. */ export async function fetchLiveVideoStreamStatus( mediaKey: string, auth: TwitterAuth, ): Promise { const baseUrl = `https://x.com/i/api/1.1/live_video_stream/status/${mediaKey}`; const queryParams = new URLSearchParams({ client: 'web', use_syndication_guest_id: 'false', cookie_set_host: 'x.com', }); const url = `${baseUrl}?${queryParams.toString()}`; const onboardingTaskUrl = 'https://api.twitter.com/1.1/onboarding/task.json'; // Retrieve necessary cookies and tokens const cookies = await auth.cookieJar().getCookies(onboardingTaskUrl); const xCsrfToken = cookies.find((cookie) => cookie.key === 'ct0'); const headers = new Headers({ Accept: '*/*', Authorization: `Bearer ${(auth as any).bearerToken}`, 'Content-Type': 'application/json', Cookie: await auth.cookieJar().getCookieString(onboardingTaskUrl), 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'x-guest-token': (auth as any).guestToken, 'x-twitter-auth-type': 'OAuth2Client', 'x-twitter-active-user': 'yes', 'x-csrf-token': xCsrfToken?.value as string, }); try { const response = await auth.fetch(url, { method: 'GET', headers: headers, }); // Update the cookie jar with any new cookies from the response await updateCookieJar(auth.cookieJar(), response.headers); // Check for errors in the response if (!response.ok) { throw new Error( `Failed to fetch live video stream status: ${await response.text()}`, ); } return await response.json(); } catch (error) { console.error( `Error fetching live video stream status for mediaKey ${mediaKey}:`, error, ); throw error; } } /** * Authenticates Periscope to obtain a token. * @param auth The authentication object. * @returns The Periscope authentication token. */ export async function fetchAuthenticatePeriscope( auth: TwitterAuth, ): Promise { const queryId = 'r7VUmxbfqNkx7uwjgONSNw'; const operationName = 'AuthenticatePeriscope'; const variables = {}; const features = {}; const variablesEncoded = encodeURIComponent(JSON.stringify(variables)); const featuresEncoded = encodeURIComponent(JSON.stringify(features)); const url = `https://x.com/i/api/graphql/${queryId}/${operationName}?variables=${variablesEncoded}&features=${featuresEncoded}`; const onboardingTaskUrl = 'https://api.twitter.com/1.1/onboarding/task.json'; const cookies = await auth.cookieJar().getCookies(onboardingTaskUrl); const xCsrfToken = cookies.find((cookie) => cookie.key === 'ct0'); if (!xCsrfToken) { throw new Error('CSRF Token (ct0) not found in cookies.'); } const clientTransactionId = generateRandomId(); const headers = new Headers({ Accept: '*/*', Authorization: `Bearer ${(auth as any).bearerToken}`, 'Content-Type': 'application/json', Cookie: await auth.cookieJar().getCookieString(onboardingTaskUrl), 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'x-guest-token': (auth as any).guestToken, 'x-twitter-auth-type': 'OAuth2Session', 'x-twitter-active-user': 'yes', 'x-csrf-token': xCsrfToken.value, 'x-client-transaction-id': clientTransactionId, 'sec-ch-ua-platform': '"Windows"', 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 'x-twitter-client-language': 'en', 'sec-ch-ua-mobile': '?0', Referer: 'https://x.com/i/spaces/start', }); try { const response = await auth.fetch(url, { method: 'GET', headers: headers, }); await updateCookieJar(auth.cookieJar(), response.headers); if (!response.ok) { const errorText = await response.text(); throw new Error(`Error ${response.status}: ${errorText}`); } const data: AuthenticatePeriscopeResponse = await response.json(); if (data.errors && data.errors.length > 0) { throw new Error(`API Errors: ${JSON.stringify(data.errors)}`); } if (!data.data.authenticate_periscope) { throw new Error('Periscope authentication failed, no data returned.'); } return data.data.authenticate_periscope; } catch (error) { console.error('Error during Periscope authentication:', error); throw error; } } /** * Logs in to Twitter via Proxsee using the Periscope JWT to obtain a login cookie. * @param jwt The JWT obtained via AuthenticatePeriscope. * @param auth The authentication object. * @returns The response containing the cookie and user information. */ export async function fetchLoginTwitterToken( jwt: unknown, auth: TwitterAuth, ): Promise { const url = 'https://proxsee.pscp.tv/api/v2/loginTwitterToken'; const idempotenceKey = generateRandomId(); const payload = { jwt: jwt, vendor_id: 'm5-proxsee-login-a2011357b73e', create_user: true, }; const headers = new Headers({ 'Content-Type': 'application/json', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', Referer: 'https://x.com/', 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 'sec-ch-ua-platform': '"Windows"', 'sec-ch-ua-mobile': '?0', 'X-Periscope-User-Agent': 'Twitter/m5', 'X-Idempotence': idempotenceKey, 'X-Attempt': '1', }); try { const response = await auth.fetch(url, { method: 'POST', headers: headers, body: JSON.stringify(payload), }); // Update the cookie jar with any new cookies from the response await updateCookieJar(auth.cookieJar(), response.headers); // Check if the response is successful if (!response.ok) { const errorText = await response.text(); throw new Error(`Error ${response.status}: ${errorText}`); } const data: LoginTwitterTokenResponse = await response.json(); if (!data.cookie || !data.user) { throw new Error('Twitter authentication failed, missing data.'); } return data; } catch (error) { console.error('Error logging into Twitter via Proxsee:', error); throw error; } }