@@ -3,12 +3,12 @@ import { genericUserAgent } from "../../config.js";
33import { createStream } from "../../stream/manage.js" ;
44import { getCookie , updateCookie } from "../cookie/manager.js" ;
55
6- const graphqlURL = 'https://api.x.com/graphql/I9GDzyCGZL2wSoYFFrrTVw/TweetResultByRestId ' ;
6+ const graphqlURL = 'https://api.x.com/graphql/4Siu98E55GquhG52zHdY5w/TweetDetail ' ;
77const tokenURL = 'https://api.x.com/1.1/guest/activate.json' ;
88
9- const tweetFeatures = JSON . stringify ( { "creator_subscriptions_tweet_preview_api_enabled" :true , "communities_web_enable_tweet_community_results_fetch" :true , "c9s_tweet_anatomy_moderator_badge_enabled" :true , "articles_preview_enabled" :true , "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" :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 , "rweb_tipjar_consumption_enabled" :true , "responsive_web_graphql_exclude_directive_enabled" :true , "verified_phone_label_enabled" :false , "responsive_web_graphql_skip_user_profile_image_extensions_enabled" :false , "responsive_web_graphql_timeline_navigation_enabled" :true , "responsive_web_enhance_cards_enabled" :false } ) ;
9+ const tweetFeatures = JSON . stringify ( { "rweb_video_screen_enabled" :false , "payments_enabled" :false , "rweb_xchat_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_imagine_annotation_enabled" :true , "responsive_web_grok_community_note_auto_translation_is_enabled" :false , "responsive_web_enhance_cards_enabled" :false } ) ;
1010
11- const tweetFieldToggles = JSON . stringify ( { "withArticleRichContentState" :true , "withArticlePlainText" :false , "withGrokAnalyze" :false } ) ;
11+ const tweetFieldToggles = JSON . stringify ( { "withArticleRichContentState" :true , "withArticlePlainText" :false , "withGrokAnalyze" :false , "withDisallowedReplyControls" : false } ) ;
1212
1313const commonHeaders = {
1414 "user-agent" : genericUserAgent ,
@@ -100,10 +100,14 @@ const requestTweet = async(dispatcher, tweetId, token, cookie) => {
100100
101101 graphqlTweetURL . searchParams . set ( 'variables' ,
102102 JSON . stringify ( {
103- tweetId,
104- withCommunity : false ,
105- includePromotedContent : false ,
106- withVoice : false
103+ focalTweetId : tweetId ,
104+ with_rux_injections : false ,
105+ rankingMode : "Relevance" ,
106+ includePromotedContent : true ,
107+ withCommunity : true ,
108+ withQuickPromoteEligibilityTweetFields : true ,
109+ withBirdwatchNotes : true ,
110+ withVoice : true
107111 } )
108112 ) ;
109113 graphqlTweetURL . searchParams . set ( 'features' , tweetFeatures ) ;
@@ -129,33 +133,55 @@ const requestTweet = async(dispatcher, tweetId, token, cookie) => {
129133 return result
130134}
131135
132- const extractGraphqlMedia = async ( tweet , dispatcher , id , guestToken , cookie ) => {
133- let tweetTypename = tweet ?. data ?. tweetResult ?. result ?. __typename ;
136+ const parseCard = ( cardOuter ) => {
137+ const card = JSON . parse (
138+ ( cardOuter ?. legacy ?. binding_values [ 0 ] . value || cardOuter ?. binding_values ?. unified_card ) ?. string_value
139+ ) ;
140+
141+ if ( ! [ "video_website" , "image_website" ] . includes ( card ?. type ) ||
142+ ! card ?. media_entities ||
143+ card ?. component_objects ?. media_1 ?. type !== "media" ) {
144+ return ;
145+ }
146+
147+ const mediaId = card . component_objects ?. media_1 ?. data ?. id ;
148+ return [ card . media_entities [ mediaId ] ] ;
149+ }
150+
151+ const extractGraphqlMedia = async ( thread , dispatcher , id , guestToken , cookie ) => {
152+ const addInsn = thread ?. data ?. threaded_conversation_with_injections_v2 ?. instructions ?. find (
153+ insn => insn . type === 'TimelineAddEntries'
154+ ) ;
155+
156+ const tweetResult = addInsn ?. entries ?. find (
157+ entry => entry . entryId === `tweet-${ id } `
158+ ) ?. content ?. itemContent ?. tweet_results ?. result ;
159+
160+ let tweetTypename = tweetResult ?. __typename ;
134161
135162 if ( ! tweetTypename ) {
136163 return { error : "fetch.empty" }
137164 }
138165
139- if ( tweetTypename === "TweetUnavailable" ) {
140- const reason = tweet ?. data ?. tweetResult ?. result ?. reason ;
141- switch ( reason ) {
142- case "Protected" :
143- return { error : "content.post.private" } ;
144- case "NsfwLoggedOut" :
145- if ( cookie ) {
146- tweet = await requestTweet ( dispatcher , id , guestToken , cookie ) ;
147- tweet = await tweet . json ( ) ;
148- tweetTypename = tweet ?. data ?. tweetResult ?. result ?. __typename ;
149- } else return { error : "content.post.age" } ;
166+ if ( tweetTypename === "TweetUnavailable" || tweetTypename === "TweetTombstone" ) {
167+ const reason = tweetResult ?. result ?. reason ;
168+ if ( reason === 'Protected' ) {
169+ return { error : "content.post.private" } ;
170+ } else if ( reason === "NsfwLoggedOut" || tweetResult ?. tombstone ?. text ?. text ?. startsWith ( 'Age-restricted' ) ) {
171+ if ( ! cookie ) {
172+ return { error : "content.post.age" } ;
173+ }
174+
175+ const tweet = await requestTweet ( dispatcher , id , guestToken , cookie ) . then ( t => t . json ( ) ) ;
176+ return extractGraphqlMedia ( tweet , dispatcher , id , guestToken ) ;
150177 }
151178 }
152179
153180 if ( ! [ "Tweet" , "TweetWithVisibilityResults" ] . includes ( tweetTypename ) ) {
154181 return { error : "content.post.unavailable" }
155182 }
156183
157- let tweetResult = tweet . data . tweetResult . result ,
158- baseTweet = tweetResult . legacy ,
184+ let baseTweet = tweetResult . legacy ,
159185 repostedTweet = baseTweet ?. retweeted_status_result ?. result . legacy . extended_entities ;
160186
161187 if ( tweetTypename === "TweetWithVisibilityResults" ) {
@@ -164,81 +190,51 @@ const extractGraphqlMedia = async (tweet, dispatcher, id, guestToken, cookie) =>
164190 }
165191
166192 if ( tweetResult . card ?. legacy ?. binding_values ?. length ) {
167- const card = JSON . parse ( tweetResult . card . legacy . binding_values [ 0 ] . value ?. string_value ) ;
168-
169- if ( ! [ "video_website" , "image_website" ] . includes ( card ?. type ) ||
170- ! card ?. media_entities ||
171- card ?. component_objects ?. media_1 ?. type !== "media" ) {
172- return ;
173- }
174-
175- const mediaId = card . component_objects ?. media_1 ?. data ?. id ;
176- return [ card . media_entities [ mediaId ] ] ;
193+ return parseCard ( tweetResult . card ) ;
177194 }
178195
179196 return ( repostedTweet ?. media || baseTweet ?. extended_entities ?. media ) ;
180197}
181198
182- const testResponse = ( result ) => {
183- const contentLength = result . headers . get ( "content-length" ) ;
184-
185- if ( ! contentLength || contentLength === '0' ) {
186- return false ;
187- }
188-
189- if ( ! result . headers . get ( "content-type" ) . startsWith ( "application/json" ) ) {
190- return false ;
191- }
192-
193- return true ;
194- }
195-
196199export default async function ( { id, index, toGif, dispatcher, alwaysProxy, subtitleLang } ) {
197200 const cookie = await getCookie ( 'twitter' ) ;
198201
199- let syndication = false ;
200-
201202 let guestToken = await getGuestToken ( dispatcher ) ;
202203 if ( ! guestToken ) return { error : "fetch.fail" } ;
203204
204- // for now we assume that graphql api will come back after some time,
205- // so we try it first
206-
207205 let tweet = await requestTweet ( dispatcher , id , guestToken ) ;
208206
209- // get new token & retry if old one expired
210- if ( [ 403 , 429 ] . includes ( tweet . status ) ) {
211- guestToken = await getGuestToken ( dispatcher , true ) ;
212- if ( cookie ) {
213- tweet = await requestTweet ( dispatcher , id , guestToken , cookie ) ;
214- } else {
215- tweet = await requestTweet ( dispatcher , id , guestToken ) ;
207+ if ( [ 403 , 404 , 429 ] . includes ( tweet . status ) ) {
208+ // get new token & retry if old one expired
209+ if ( [ 403 , 429 ] . includes ( tweet . status ) ) {
210+ guestToken = await getGuestToken ( dispatcher , true ) ;
216211 }
212+ tweet = await requestTweet ( dispatcher , id , guestToken , cookie ) ;
217213 }
218214
219- const testGraphql = testResponse ( tweet ) ;
215+ let media ;
216+ try {
217+ tweet = await tweet . json ( ) ;
218+ media = await extractGraphqlMedia ( tweet , dispatcher , id , guestToken , cookie ) ;
219+ } catch { }
220220
221221 // if graphql requests fail, then resort to tweet embed api
222- if ( ! testGraphql ) {
223- syndication = true ;
224- tweet = await requestSyndication ( dispatcher , id ) ;
222+ if ( ! media || 'error' in media ) {
223+ try {
224+ tweet = await requestSyndication ( dispatcher , id ) ;
225+ tweet = await tweet . json ( ) ;
225226
226- const testSyndication = testResponse ( tweet ) ;
227+ if ( tweet ?. card ) {
228+ media = parseCard ( tweet . card ) ;
229+ }
230+ } catch { }
227231
228- // if even syndication request failed, then cry out loud
229- if ( ! testSyndication ) {
230- return { error : "fetch.fail" } ;
231- }
232+ media = tweet ?. mediaDetails ?? media ;
232233 }
233234
234- tweet = await tweet . json ( ) ;
235-
236- let media =
237- syndication
238- ? tweet . mediaDetails
239- : await extractGraphqlMedia ( tweet , dispatcher , id , guestToken , cookie ) ;
240-
241- if ( ! media ) return { error : "fetch.empty" } ;
235+ if ( ! media || 'error' in media ) {
236+ return { error : media ?. error || "fetch.empty" } ;
237+ }
242238
243239 // check if there's a video at given index (/video/<index>)
244240 if ( index >= 0 && index < media ?. length ) {
0 commit comments