import type { Tweet, TweetList, TweetResult, TwitterBookmarkResponse, TwitterList, TwitterListTimelineResponse, TwitterListsManagementResponse, TwitterNotification, TwitterNotificationsTimelineResponse, TimelineEntry, TwitterTimelineResponse, TwitterTweetDetailResponse, TwitterUserTweetsResponse, APITwitterList, TweetWithVisibilityResult, TwitterUser, RTMetadata, TwitterProfilesResponse, UserResult, } from "./types"; import { TransactionIdGenerator } from "./python"; const TWITTER_INTERNAL_API_KEY = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; export class TwitterApiService { cookie: string; constructor(cookie: string) { this.cookie = cookie; } // Read endpoints private static readonly BOOKMARKS_URL = new URL( "https://x.com/i/api/graphql/C7CReOA1R0PwKorWAxnNUQ/Bookmarks", ); private static readonly FOLLOWING_URL = new URL( "https://x.com/i/api/graphql/fhqL7Cgmvax9jOhRMOhWpA/HomeLatestTimeline", ); private static readonly FORYOU_URL = new URL( "https://x.com/i/api/graphql/sMNeM4wvNe4JnRUZ2jd2zw/HomeTimeline", ); private static readonly LISTS_URL = new URL( "https://x.com/i/api/graphql/wLXb5F6pIEOrYtTjXFLQsA/ListsManagementPageTimeline", ); private static readonly LIST_URL = new URL( "https://x.com/i/api/graphql/p-5fXSlJaR-aZ4UUBdPMAg/ListLatestTweetsTimeline", ); private static readonly NOTIFICATIONS_URL = new URL( "https://api.x.com/1.1/notifications/timeline.json", ); private static readonly USERDATA_URL = new URL( "https://x.com/i/api/graphql/2AtIgw7Kz26sV6sEBrQjSQ/UsersByRestIds", ); private static readonly USER_URL = new URL( "https://x.com/i/api/graphql/Le1DChzkS7ioJH_yEPMi3w/UserTweets", ); private static readonly THREAD_URL = new URL( "https://x.com/i/api/graphql/aTYmkYpjWyvUyrinVWSiYA/TweetDetail", ); private static readonly HEADERS = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", Accept: "*/*", Referer: "https://x.com/i/bookmarks", "Content-Type": "application/json", "X-Twitter-Auth-Type": "OAuth2Session", "X-Twitter-Active-User": "yes", "X-Twitter-Client-Language": "en", }; private get csrfToken() { return this.cookie.match(/ct0=([^;]+)/)?.[1]; } private buildHeaders(extra?: Record) { const headers: Record = { ...TwitterApiService.HEADERS, Authorization: TWITTER_INTERNAL_API_KEY, Cookie: this.cookie, ...(extra ?? {}), }; const csrf = this.csrfToken; if (csrf) headers["X-Csrf-Token"] = csrf; return headers; } private async request(url: URL, init: RequestInit) { const headers = this.buildHeaders(init.headers as Record); const xcs = new TransactionIdGenerator(""); await xcs.init(); const xclientid = await xcs.getTransactionId(init.method!, url.pathname); headers["X-Client-Transaction-Id"] = xclientid; const response = await fetch(url, { ...init, headers }); if (!response.ok) { console.log(headers); console.log(response); throw new Error( `Twitter API request failed: ${response.status} ${response.statusText}`, ); } return await response.json(); } async postCall(url: URL, payload: Record) { const body = JSON.stringify(payload); return this.request(url, { method: "POST", body, headers: { "Content-Type": "application/json" }, }); } async getCall(url: URL) { return this.request(url, { method: "GET" }); } async findOwn(): Promise { const cookie = decodeURIComponent(this.cookie); const match = cookie.match(/twid=u=([^;]+)/); const id = match![1]!; const profs = await this.fetchProfiles([id]); return profs[0]!; } async fetchProfiles(userIds: string[]): Promise { const variables = { userIds, }; const features = { payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, responsive_web_graphql_timeline_navigation_enabled: true, }; const url = new URL(TwitterApiService.USERDATA_URL); url.searchParams.set("variables", JSON.stringify(variables)); url.searchParams.set("features", JSON.stringify(features)); const data = (await this.getCall(url)) as TwitterProfilesResponse; const users = data?.data?.users; if (!users) throw new Error("error parsing ids"); const parsed = users.map((u) => TwitterApiService.extractUserData(u.result), ); return parsed; } async fetchForyou(cursor?: string): Promise { const payload = { variables: { count: 50, cursor: cursor || "DAABCgABGv-ytnDAJxEKAAIa_qVgidrhcwgAAwAAAAEAAA", includePromotedContent: true, latestControlAvailable: true, withCommunity: true, seenTweetIds: [], }, features: { rweb_video_screen_enabled: false, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, 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, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }, queryId: "sMNeM4wvNe4JnRUZ2jd2zw", }; const data = (await this.postCall( TwitterApiService.FORYOU_URL, payload, )) as TwitterTimelineResponse; try { return TwitterApiService.parseTimelineResponse(data, "foryou"); } catch (e) { console.error(e); console.dir(data, { depth: null }); throw new Error("wtf"); } } async fetchFollowing(cursor?: string) { const payload = { variables: { count: 50, cursor: cursor || "DAABCgABGv-ytnDAJxEKAAIa_qVgidrhcwgAAwAAAAEAAA", includePromotedContent: true, latestControlAvailable: true, requestContext: "launch", seenTweetIds: [], }, features: { rweb_video_screen_enabled: false, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, 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, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }, queryId: "fhqL7Cgmvax9jOhRMOhWpA", }; const data = (await this.postCall( TwitterApiService.FOLLOWING_URL, payload, )) as TwitterTimelineResponse; try { return TwitterApiService.parseTimelineResponse(data, "following"); } catch (e) { console.error(e); console.dir(data, { depth: null }); throw new Error("wtf"); } } async fetchList(listId: string, cursor?: string): Promise { const variables = { listId, count: 20, cursor }; const features = { rweb_video_screen_enabled: false, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, 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, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }; const url = new URL(TwitterApiService.LIST_URL); url.searchParams.set("variables", JSON.stringify(variables)); url.searchParams.set("features", JSON.stringify(features)); const data = (await this.getCall(url)) as TwitterListTimelineResponse; return TwitterApiService.parseListTimelineResponse(data); } async fetchLists(): Promise { const variables = { count: 100 }; const features = { rweb_video_screen_enabled: false, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, 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, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }; const url = new URL(TwitterApiService.LISTS_URL); url.searchParams.set("variables", JSON.stringify(variables)); url.searchParams.set("features", JSON.stringify(features)); const data = (await this.getCall(url)) as TwitterListsManagementResponse; try { return TwitterApiService.parseListsManagementResponse(data); } catch (e) { console.error(e); // console.dir(data.data.viewer.list_management_timeline, { depth: null }); throw e; } } async fetchNotifications(cursor?: string): Promise { const variables: Record = { include_profile_interstitial_type: 1, include_blocking: 1, include_blocked_by: 1, include_followed_by: 1, include_want_retweets: 1, include_mute_edge: 1, include_can_dm: 1, include_can_media_tag: 1, include_ext_has_nft_avatar: 1, include_ext_is_blue_verified: 1, include_ext_verified_type: 1, include_ext_profile_image_shape: 1, skip_status: 1, cards_platform: "Web-12", include_cards: 1, include_composer_source: "true", include_ext_alt_text: "true", include_ext_limited_action_results: "false", include_reply_count: 1, tweet_mode: "extended", include_entities: "true", include_user_entities: "true", include_ext_media_color: "true", include_ext_media_availability: "true", include_ext_sensitive_media_warning: "true", include_ext_trusted_friends_metadata: "true", send_error_codes: "true", simple_quoted_tweet: "true", count: 40, ext: "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,enrichments,superFollowMetadata,unmentionInfo,editControl,vibe", }; if (cursor) { variables.cursor = cursor; } const url = new URL(TwitterApiService.NOTIFICATIONS_URL); Object.keys(variables).forEach((key) => url.searchParams.set(key, variables[key]!.toString()), ); const data = (await this.getCall( url, )) as TwitterNotificationsTimelineResponse; return TwitterApiService.parseNotificationsResponse(data); } async fetchUserTweets(userId: string, cursor?: string): Promise { const variables = { userId, count: 50, includePromotedContent: true, withQuickPromoteEligibilityTweetFields: true, withVoice: true, }; const features = { rweb_video_screen_enabled: false, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, 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, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }; const fieldToggles = { withArticlePlainText: true }; const url = new URL(TwitterApiService.USER_URL); url.searchParams.set("variables", JSON.stringify(variables)); url.searchParams.set("features", JSON.stringify(features)); url.searchParams.set("fieldToggles", JSON.stringify(fieldToggles)); const data = (await this.getCall(url)) as TwitterUserTweetsResponse; try { return TwitterApiService.parseUserTweetsResponse(data); } catch (e) { console.error(e); console.dir(data, { depth: null }); throw new Error("bad"); } } async fetchThread(tweetId: string, cursor?: string): Promise { const variables = { focalTweetId: tweetId, referrer: "profile", with_rux_injections: false, rankingMode: "Relevance", includePromotedContent: true, withCommunity: true, withQuickPromoteEligibilityTweetFields: true, withBirdwatchNotes: true, withVoice: true, }; const features = { rweb_video_screen_enabled: false, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, 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, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }; const fieldToggles = { withArticleRichContentState: true, withArticlePlainText: true, withGrokAnalyze: true, withDisallowedReplyControls: false, }; const url = new URL(TwitterApiService.THREAD_URL); url.searchParams.set("variables", JSON.stringify(variables)); url.searchParams.set("features", JSON.stringify(features)); url.searchParams.set("fieldToggles", JSON.stringify(fieldToggles)); const data = (await this.getCall(url)) as TwitterTweetDetailResponse; try { return TwitterApiService.parseThreadResponse(data); } catch (e) { console.error(e); console.dir(data, { depth: null }); throw new Error("Bad"); } } async fetchBookmarks(cursor?: string): Promise { const variables = { count: 40, cursor: cursor || null, includePromotedContent: true, }; const features = { rweb_video_screen_enabled: false, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_enabled: true, articles_preview_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, 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, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, responsive_web_grok_image_annotation_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_enhance_cards_enabled: false, }; const url = new URL(TwitterApiService.BOOKMARKS_URL); url.searchParams.set("variables", JSON.stringify(variables)); url.searchParams.set("features", JSON.stringify(features)); const data = (await this.getCall(url)) as TwitterBookmarkResponse; return TwitterApiService.parseBookmarkResponse(data); } private static parseUserTweetsResponse( response: TwitterUserTweetsResponse, ): TweetList { const tweets: Tweet[] = []; let cursorBottom = ""; let cursorTop = ""; const instructions = response.data?.user?.result?.timeline?.timeline?.instructions; if (!instructions || !Array.isArray(instructions)) throw new Error("error parsing user feed"); for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries") { if (!instruction.entries || !Array.isArray(instruction.entries)) throw new Error("error parsing user feed"); for (const entry of instruction.entries) { if (entry.content?.entryType === "TimelineTimelineItem") { const tweetData = entry.content.itemContent?.tweet_results?.result; if (tweetData) { const tweet = this.extractTweetData(tweetData); if (tweet) { tweets.push(tweet); } } } else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Bottom" ) cursorBottom = entry.content?.value || ""; else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Top" ) cursorTop = entry.content?.value || ""; } } } return { tweets, cursorBottom, cursorTop }; } private static parseThreadResponse( response: TwitterTweetDetailResponse, ): TweetList { const tweets: Tweet[] = []; let cursorBottom = ""; let cursorTop = ""; const instructions = response.data?.threaded_conversation_with_injections_v2?.instructions; if (!instructions || !Array.isArray(instructions)) throw new Error("error parsing thread "); for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries") { if (!instruction.entries || !Array.isArray(instruction.entries)) throw new Error("error parsing thread feed"); for (const entry of instruction.entries) { if (entry.content?.entryType === "TimelineTimelineItem") { const tweetData = entry.content.itemContent?.tweet_results?.result; if (tweetData) { const tweet = this.extractTweetData(tweetData); if (tweet) { tweets.push(tweet); } } } else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Bottom" ) cursorBottom = entry.content?.value || ""; else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Top" ) cursorTop = entry.content?.value || ""; } } } return { tweets, cursorBottom, cursorTop }; } private static parseBookmarkResponse( response: TwitterBookmarkResponse, ): TweetList { const tweets: Tweet[] = []; let cursorBottom = ""; let cursorTop = ""; const instructions = response.data?.bookmark_timeline_v2?.timeline?.instructions || []; for (const instruction of instructions) { if ( instruction.type === "TimelineAddEntries" || instruction.type === "TimelineReplaceEntry" || instruction.type === "TimelineShowMoreEntries" ) { const entries = [ ...(instruction.entries || []), ...(((instruction as { entry?: TimelineEntry }).entry ? [(instruction as { entry?: TimelineEntry }).entry!] : []) as TimelineEntry[]), ]; for (const entry of entries) { const content = entry.content; if (content?.entryType === "TimelineTimelineItem") { const tweetData = content.itemContent?.tweet_results?.result; if (tweetData) { const bookmark = this.extractTweetData(tweetData); if (bookmark) { tweets.push(bookmark); } } else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Bottom" ) cursorBottom = entry.content?.value || ""; else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Top" ) cursorTop = entry.content?.value || ""; } } } } return { tweets, cursorTop, cursorBottom, }; } private static parseTimelineResponse( response: TwitterTimelineResponse, type: "foryou" | "following", ): TweetList { const tweets: Tweet[] = []; let cursorBottom = ""; let cursorTop = ""; const instructions = response.data?.home?.home_timeline_urt?.instructions; response.data?.home_timeline_urt?.instructions; if (!instructions || !Array.isArray(instructions)) throw new Error("error parsing thread "); for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries") { if (!instruction.entries || !Array.isArray(instruction.entries)) throw new Error("error parsing thread feed"); for (const entry of instruction.entries) { // if (entry.content.entryType.includes("ursor")) console.log(entry); // if (entry.content.entryType.includes("odule")) // console.log("module", entry); if (entry.content?.entryType === "TimelineTimelineItem") { const tweetData = entry.content?.itemContent?.tweet_results?.result; if (tweetData) { try { const tweet = this.extractTweetData(tweetData); tweets.push(tweet); } catch (e) { console.error(e); // console.dir(entry, { depth: null }); } } } else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Bottom" ) cursorBottom = entry.content?.value || ""; else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Gap" // TODO wtf??? ) cursorBottom = entry.content?.value || ""; else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Top" ) cursorTop = entry.content?.value || ""; } } } return { tweets, cursorTop, cursorBottom }; } private static parseListTimelineResponse( response: TwitterListTimelineResponse, ): TweetList { const tweets: Tweet[] = []; let cursorBottom = ""; let cursorTop = ""; const instructions = response.data?.list?.tweets_timeline?.timeline?.instructions; if (!instructions || !Array.isArray(instructions)) throw new Error("error parsing tw timeline res"); for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries") { if (!instruction.entries || !Array.isArray(instruction.entries)) throw new Error("error parsing tw timeline res"); for (const entry of instruction.entries) { if (entry.content?.entryType === "TimelineTimelineItem") { const tweetData = entry.content.itemContent?.tweet_results?.result; if (tweetData) { const tweet = this.extractTweetData(tweetData); tweets.push(tweet); } } else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Bottom" ) cursorBottom = entry.content?.value || ""; else if ( entry.content?.entryType === "TimelineTimelineCursor" && entry.content?.cursorType === "Top" ) cursorTop = entry.content?.value || ""; } } } return { tweets, cursorBottom, cursorTop }; } private static parseListsManagementResponse( response: TwitterListsManagementResponse, ): TwitterList[] { const lists: TwitterList[] = []; const instructions = response.data?.viewer?.list_management_timeline?.timeline?.instructions; if (!instructions || !Array.isArray(instructions)) throw new Error("error parsing tw lists res"); for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries") { if (!instruction?.entries || !Array.isArray(instruction.entries)) throw new Error("error parsing tw lists res 2"); for (const entry of instruction.entries) { console.log("entry", entry.content.__typename); // if (entry.content.__typename === "TimelineTimelineModule") if (entry.content.__typename === "TimelineTimelineCursor") { console.dir(entry, { depth: null }); continue; } // entry.content.entryType can be TimelineTimelineModule, TimelineTimelineCursor, // entry.entryId can be list-to-follow- which si the recommended lists that show on top // or owned-subscribed-list-module- which is what we want const listList = entry?.content?.items; if (!listList || !Array.isArray(listList)) throw new Error("error parsing tw lists res 3"); for (const list of listList) { lists.push(this.parseListResponse(list.item.itemContent.list)); } } } } return lists; } private static parseListResponse(res: APITwitterList): TwitterList { const { name, id_str, member_count, subscriber_count } = res; const creator = res.user_results.result.core.name; return { name, id: id_str, member_count, subscriber_count, creator }; } private static parseNotificationsResponse( response: TwitterNotificationsTimelineResponse, ): TwitterNotification[] { const notifications: TwitterNotification[] = []; const timelineNotifs = response.timeline.instructions[0]?.addEntries?.entries; if (!timelineNotifs || !Array.isArray(timelineNotifs)) throw new Error("error parsing notifs"); for (const entry of timelineNotifs) { const notificationId = entry.content.notification.id; const notification = response.globalObjects.notifications[notificationId]; if (notification) { notifications.push(notification); } } return notifications; } private static extractUserData(userResults: UserResult): TwitterUser { return { id: userResults.rest_id, avatar: userResults.avatar?.image_url || userResults.legacy?.profile_image_url_https!, name: userResults.legacy?.name || userResults.core?.name!, username: userResults.legacy?.screen_name || userResults.core?.screen_name!, }; } private static extractTweetData( tweetRes: TweetResult | TweetWithVisibilityResult, rter: RTMetadata | null = null, ): Tweet { const tweetData = tweetRes.__typename === "Tweet" ? tweetRes : tweetRes.tweet; console.log({ tweetData }); let quoting: Tweet | null = null; let retweeted_by = rter; const legacy = tweetData?.legacy; const userResults = tweetData?.core?.user_results?.result; // if (!legacy || !userResults) throw new Error("no legacy??"); if (!legacy) throw new Error("no legacy??"); if (!userResults) throw new Error("no userResults??"); const author = this.extractUserData(userResults); const time = new Date(legacy.created_at).getTime(); // is_rt if (legacy.retweeted_status_result) { const d = legacy.retweeted_status_result.result; if (!d) console.log("bad rt", tweetData); return this.extractTweetData(legacy.retweeted_status_result.result, { author, time, }); } // // quotes if ( tweetData.quoted_status_result && tweetData.quoted_status_result.result ) { // const d = tweetData.quoted_status_result.result; // if (!d) console.log("bad quote", tweetData); quoting = this.extractTweetData(tweetData.quoted_status_result.result); } // const mediaEntities = legacy.entities.media; // if (!mediaEntities) { // console.log("no media"); // console.dir(legacy.entities, { depth: null }); // } const media = (mediaEntities || []).reduce( ( acc: { pics: string[]; video: { thumb: string; url: string } }, item, ) => { if (item.type === "photo" && item.media_url_https) { return { pics: [...acc.pics, item.media_url_https], video: acc.video, }; } if (item.type === "video" && item.video_info?.variants) { const video = item.video_info.variants.reduce( ( acc: { bitrate?: number; url: string }, vid: { bitrate?: number; url: string }, ) => { if (!vid.bitrate) return acc; if (!acc.bitrate || vid.bitrate > acc.bitrate) return vid; return acc; }, { url: "" }, ); return { pics: acc.pics, video: { url: video.url!, thumb: item.media_url_https!, }, }; } return acc; }, { pics: [] as string[], video: { thumb: "", url: "" } }, ); if (legacy.full_text.includes("your computers")) console.dir(tweetRes, { depth: null }); const replyingTo = legacy.entities?.user_mentions ? legacy.entities.user_mentions.map((m) => ({ name: m.name, username: m.screen_name, id: m.id_str, })) : []; return { id: tweetData.rest_id, text: legacy.display_text_range ? legacy.full_text.slice( legacy.display_text_range[0], legacy.display_text_range[1] + 1, ) : legacy.full_text!, language: legacy.lang || "en", author, time, urls: legacy.entities?.urls?.map( (url: { expanded_url: string; display_url: string }) => ({ expandedUrl: url.expanded_url, displayUrl: url.display_url, }), ) || [], media, hashtags: legacy.entities?.hashtags?.map((tag: { text: string }) => tag.text) || [], quoting, retweeted_by, liked: legacy.favorited, bookmarked: legacy.bookmarked, rted: legacy.retweeted, replyingTo, }; } async fetchAllBookmarks(): Promise { const allBookmarks: Tweet[] = []; let cursor: string | undefined; let hasMore = true; while (hasMore) { try { const result = await this.fetchBookmarks(cursor); allBookmarks.push(...result.tweets); cursor = result.cursorBottom; // Rate limiting - be nice to Twitter's API await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { console.error("Error fetching bookmarks batch:", error); break; } } return allBookmarks; } // WRITE ENDPOINTS // TODO Grok stuff // // TODO add images, polls etc. // quote is the URL https;//x.com/{user}/status/{id} of the quoted tweet; async createTweet(text: string, topts: { quote?: string; reply?: string }) { const queryId = `-fU2A9SG7hdlzUdOh04POw`; const url = `https://x.com/i/api/graphql/${queryId}/createTweet`; let variables: any = { tweet_text: text, dark_request: false, media: { media_entities: [], possibly_sensitive: false, }, semantic_annotation_ids: [], disallowed_reply_options: null, }; const features = { 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: false, responsive_web_grok_analyze_post_followups_enabled: true, responsive_web_jetfuel_frame: true, responsive_web_grok_share_attachment_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: true, tweet_awards_web_tipping_enabled: false, responsive_web_grok_show_grok_translated_post: false, responsive_web_grok_analysis_button_from_backend: true, creator_subscriptions_quote_tweet_preview_enabled: false, longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: true, payments_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true, responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: true, verified_phone_label_enabled: false, articles_preview_enabled: true, responsive_web_grok_community_note_auto_translation_is_enabled: false, responsive_web_graphql_skip_user_profile_image_extensions_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_grok_image_annotation_enabled: true, responsive_web_grok_imagine_annotation_enabled: true, responsive_web_graphql_timeline_navigation_enabled: true, responsive_web_enhance_cards_enabled: false, }; if (topts.reply) variables = { ...variables, reply: { in_reply_to_tweet_id: topts.reply, exclude_reply_user_ids: [], }, }; if (topts.quote) variables = { ...variables, attachment_url: topts.quote }; const params = { ...features, variables, queryId }; const body = JSON.stringify(params); const nheaders = { "Content-type": "application/json" }; const headers = { ...TwitterApiService.HEADERS, Authorization: TWITTER_INTERNAL_API_KEY, Cookie: this.cookie, ...nheaders, }; const opts = { method: "POST", headers, body, }; const res = await fetch(url, opts); console.log("like added", res); } async addLike(tweet_id: string) { const queryId = `lI07N6Otwv1PhnEgXILM7A`; const url = new URL(`https://x.com/i/api/graphql/${queryId}/FavoriteTweet`); const body = { variables: { tweet_id }, queryId }; return await this.postCall(url, body); } async removeLike(tweet_id: string) { const queryId = `ZYKSe-w7KEslx3JhSIk5LA`; const url = new URL( `https://x.com/i/api/graphql/${queryId}/UnfavoriteTweet`, ); const body = { variables: { tweet_id }, queryId }; return await this.postCall(url, body); } async addRT(tweet_id: string) { const queryId = `ZYKSe-w7KEslx3JhSIk5LA`; const url = new URL(`https://x.com/i/api/graphql/${queryId}/CreateRetweet`); // TODO wtf is dark_request bruh const body = { variables: { tweet_id, dark_request: false }, queryId, }; return await this.postCall(url, body); } async removeRT(tweet_id: string) { const queryId = `iQtK4dl5hBmXewYZuEOKVw`; const url = new URL(`https://x.com/i/api/graphql/${queryId}/DeleteRetweet`); const body = { variables: { tweet_id, dark_request: false }, queryId, }; return await this.postCall(url, body); } async removeTweet(tweet_id: string) { const queryId = `VaenaVgh5q5ih7kvyVjgtg`; const url = new URL(`https://x.com/i/api/graphql/${queryId}/DeleteTweet`); const body = { variables: { tweet_id, dark_request: false }, queryId, }; return await this.postCall(url, body); } async addBookmark(tweet_id: string) { const queryId = `aoDbu3RHznuiSkQ9aNM67Q`; const url = new URL( `https://x.com/i/api/graphql/${queryId}/CreateBookmark`, ); const body = { variables: { tweet_id }, queryId }; try { const res = await this.postCall(url, body); return res?.data?.tweet_bookmark_put === "Done"; } catch (e) { console.log("wtf man", e); // return this.removeBookmark(tweet_id); } } async removeBookmark(tweet_id: string) { const queryId = `Wlmlj2-xzyS1GN3a6cj-mQ`; const url = new URL( `https://x.com/i/api/graphql/${queryId}/DeleteBookmark`, ); const body = { variables: { tweet_id }, queryId }; const res = await this.postCall(url, body); return res?.data?.tweet_bookmark_delete === "Done"; } }