@@ -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,56 @@ 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
139+ || cardOuter ?. binding_values ?. unified_card ) ?. string_value ,
140+ ) ;
141+
142+ if ( ! [ "video_website" , "image_website" ] . includes ( card ?. type )
143+ || ! card ?. media_entities
144+ || card ?. component_objects ?. media_1 ?. type !== "media" ) {
145+ return ;
146+ }
147+
148+ const mediaId = card . component_objects ?. media_1 ?. data ?. id ;
149+ return [ card . media_entities [ mediaId ] ] ;
150+ } ;
151+
152+ const extractGraphqlMedia = async ( thread , dispatcher , id , guestToken , cookie ) => {
153+ const addInsn = thread ?. data ?. threaded_conversation_with_injections_v2 ?. instructions ?. find (
154+ insn => insn . type === 'TimelineAddEntries'
155+ ) ;
156+
157+ const tweetResult = addInsn ?. entries ?. find (
158+ entry => entry . entryId === `tweet-${ id } `
159+ ) ?. content ?. itemContent ?. tweet_results ?. result ;
160+
161+ let tweetTypename = tweetResult ?. __typename ;
134162
135163 if ( ! tweetTypename ) {
136164 return { error : "fetch.empty" }
137165 }
138166
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" } ;
167+ if ( tweetTypename === "TweetUnavailable" || tweetTypename === "TweetTombstone" ) {
168+ const reason = tweetResult ?. result ?. reason ;
169+ if ( reason === 'Protected' ) {
170+ return { error : "content.post.private" } ;
171+ } else if ( reason === "NsfwLoggedOut" || tweetResult ?. tombstone ?. text ?. text ?. startsWith ( 'Age-restricted' ) ) {
172+ if ( ! cookie ) {
173+ return { error : "content.post.age" } ;
174+ }
175+
176+ const tweet = await requestTweet ( dispatcher , id , guestToken , cookie ) . then ( t => t . json ( ) ) ;
177+ return extractGraphqlMedia ( tweet , dispatcher , id , guestToken ) ;
150178 }
151179 }
152180
153181 if ( ! [ "Tweet" , "TweetWithVisibilityResults" ] . includes ( tweetTypename ) ) {
154182 return { error : "content.post.unavailable" }
155183 }
156184
157- let tweetResult = tweet . data . tweetResult . result ,
158- baseTweet = tweetResult . legacy ,
185+ let baseTweet = tweetResult . legacy ,
159186 repostedTweet = baseTweet ?. retweeted_status_result ?. result . legacy . extended_entities ;
160187
161188 if ( tweetTypename === "TweetWithVisibilityResults" ) {
@@ -164,81 +191,51 @@ const extractGraphqlMedia = async (tweet, dispatcher, id, guestToken, cookie) =>
164191 }
165192
166193 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 ] ] ;
194+ return parseCard ( tweetResult . card ) ;
177195 }
178196
179197 return ( repostedTweet ?. media || baseTweet ?. extended_entities ?. media ) ;
180198}
181199
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-
196200export default async function ( { id, index, toGif, dispatcher, alwaysProxy, subtitleLang } ) {
197201 const cookie = await getCookie ( 'twitter' ) ;
198202
199- let syndication = false ;
200-
201203 let guestToken = await getGuestToken ( dispatcher ) ;
202204 if ( ! guestToken ) return { error : "fetch.fail" } ;
203205
204- // for now we assume that graphql api will come back after some time,
205- // so we try it first
206-
207206 let tweet = await requestTweet ( dispatcher , id , guestToken ) ;
208207
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 ) ;
208+ if ( [ 403 , 404 , 429 ] . includes ( tweet . status ) ) {
209+ // get new token & retry if old one expired
210+ if ( [ 403 , 429 ] . includes ( tweet . status ) ) {
211+ guestToken = await getGuestToken ( dispatcher , true ) ;
216212 }
213+ tweet = await requestTweet ( dispatcher , id , guestToken , cookie ) ;
217214 }
218215
219- const testGraphql = testResponse ( tweet ) ;
216+ let media ;
217+ try {
218+ tweet = await tweet . json ( ) ;
219+ media = await extractGraphqlMedia ( tweet , dispatcher , id , guestToken , cookie ) ;
220+ } catch { }
220221
221222 // if graphql requests fail, then resort to tweet embed api
222- if ( ! testGraphql ) {
223- syndication = true ;
224- tweet = await requestSyndication ( dispatcher , id ) ;
223+ if ( ! media || 'error' in media ) {
224+ try {
225+ tweet = await requestSyndication ( dispatcher , id ) ;
226+ tweet = await tweet . json ( ) ;
225227
226- const testSyndication = testResponse ( tweet ) ;
228+ if ( tweet ?. card ) {
229+ media = parseCard ( tweet . card ) ;
230+ }
231+ } catch { }
227232
228- // if even syndication request failed, then cry out loud
229- if ( ! testSyndication ) {
230- return { error : "fetch.fail" } ;
231- }
233+ media = tweet ?. mediaDetails ?? media ;
232234 }
233235
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" } ;
236+ if ( ! media || 'error' in media ) {
237+ return { error : media ?. error || "fetch.empty" } ;
238+ }
242239
243240 // check if there's a video at given index (/video/<index>)
244241 if ( index >= 0 && index < media ?. length ) {
0 commit comments