agent-twitter-summary
Version:
A twitter client for agents
450 lines (376 loc) • 16.4 kB
text/typescript
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<AudioSpace> {
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<Subtopic[]> {
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<Community[]> {
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<LiveVideoStreamStatus> {
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<string> {
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<LoginTwitterTokenResponse> {
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;
}
}