const TWITTER_INTERNAL_API_KEY = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; interface TwitterBookmarkResponse { data: { bookmark_timeline_v2: { timeline: { instructions: Array<{ type: string; entries?: Array<{ content: { entryType: string; itemContent?: { tweet_results: { result: { rest_id: string; core: { user_results: { result: { name: string; screen_name: string; }; }; }; legacy: { full_text: string; created_at: string; entities: { urls?: Array<{ expanded_url: string; display_url: string; }>; media?: Array<{ expanded_url: string; display_url: string; media_url_https: string; media_key: string; type: "photo" | "string"; }>; hashtags?: Array<{ text: string; }>; }; }; }; }; }; }; }>; }>; }; }; }; } export interface TwitterBookmark { id: string; text: string; language: string; author: { avatar: string; name: string; username: string; }; createdAt: string; urls: Array<{ expandedUrl: string; displayUrl: string; }>; media: { pics: string[]; video: { thumb: string; url: string } }; hashtags: string[]; } export class TwitterApiService { cookie: string; constructor(cookie: string) { this.cookie = cookie; } private static readonly BOOKMARKS_URL = "https://x.com/i/api/graphql/C7CReOA1R0PwKorWAxnNUQ/Bookmarks"; private static readonly 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, }; 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", }; async fetchBookmarks(cursor?: string): Promise<{ bookmarks: TwitterBookmark[]; nextCursor?: string; hasMore: boolean; }> { const variables = { count: 40, cursor: cursor || null, includePromotedContent: true, }; const uri = new URL(TwitterApiService.BOOKMARKS_URL); uri.searchParams.set("variables", JSON.stringify(variables)); uri.searchParams.set( "features", JSON.stringify(TwitterApiService.FEATURES), ); const url = uri.toString(); const csrfm = this.cookie.match(/ct0=([^;]+)/); const csrf = csrfm![1]; const nheaders = { "X-Csrf-Token": csrf! }; const headers = { ...TwitterApiService.HEADERS, Authorization: TWITTER_INTERNAL_API_KEY, Cookie: this.cookie, ...nheaders, }; const response = await fetch(url, { method: "GET", headers, }); if (!response.ok) { throw new Error( `Twitter API request failed: ${response.status} ${response.statusText}`, ); } const data = (await response.json()) as TwitterBookmarkResponse; return TwitterApiService.parseBookmarkResponse(data); } private static parseBookmarkResponse(response: TwitterBookmarkResponse): { bookmarks: TwitterBookmark[]; nextCursor?: string; hasMore: boolean; } { const bookmarks: TwitterBookmark[] = []; let nextCursor: string | undefined; let hasMore = false; const instructions = response.data?.bookmark_timeline_v2?.timeline?.instructions || []; for (const instruction of instructions) { if (instruction.type === "TimelineAddEntries") { for (const entry of instruction.entries || []) { if (entry.content.entryType === "TimelineTimelineItem") { const tweetData = entry.content.itemContent?.tweet_results?.result; if (tweetData) { const bookmark = this.extractBookmarkData(tweetData); if (bookmark) { bookmarks.push(bookmark); } } } } } } const ret = { bookmarks, hasMore }; return nextCursor ? { ...ret, nextCursor } : ret; } private static extractBookmarkData(tweetData: any): TwitterBookmark | null { try { const legacy = tweetData.legacy; const userResults = tweetData.core?.user_results?.result; if (!legacy || !userResults) { return null; } const mediaEntities = legacy.entities?.media || []; const media = mediaEntities.reduce( (acc: { pics: string[]; video: string }, item: any) => { if (item.type === "photo") return { pics: [...acc.pics, item.media_url_https], video: acc.video, }; if (item.type === "video") { const video = item.video_info.variants.reduce( (acc: any, vid: any) => { if (!vid.bitrate) return acc; if (vid.bitrate > acc.bitrate) return vid; else return acc; }, {}, ); return { pics: acc.pics, video: { url: video.url, thumb: video.media_url_https }, }; } }, { pics: [], video: { thumb: "", url: "" } }, ); return { id: tweetData.rest_id, text: legacy.full_text.slice( legacy.display_text_range[0], legacy.display_text_range[1] + 1, ), language: legacy.lang, author: { avatar: userResults.avatar.image_url, name: userResults.core.name, username: userResults.core.screen_name, }, createdAt: legacy.created_at, urls: legacy.entities?.urls?.map((url: any) => ({ expandedUrl: url.expanded_url, displayUrl: url.display_url, })) || [], media, hashtags: legacy.entities?.hashtags?.map((tag: any) => tag.text) || [], }; } catch (error) { console.error("Error extracting bookmark data:", error); return null; } } async fetchAllBookmarks(): Promise { const allBookmarks: TwitterBookmark[] = []; let cursor: string | undefined; let hasMore = true; while (hasMore) { try { const result = await this.fetchBookmarks(cursor); allBookmarks.push(...result.bookmarks); cursor = result.nextCursor; hasMore = result.hasMore && !!cursor; // 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; } async removeBookmark(tweet_id: string) { const queryId = `Wlmlj2-xzyS1GN3a6cj-mQ`; const url = `https://x.com/i/api/graphql/${queryId}/DeleteBookmark`; const body = JSON.stringify({ variables: { tweet_id }, queryId }); 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("bookmark deleted", res); } }