@@ -2,8 +2,16 @@ import { cli, Strategy } from '../../registry.js';
22
33// ── Twitter GraphQL constants ──────────────────────────────────────────
44
5- const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' ;
5+ const BEARER_TOKEN =
6+ 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' ;
67const HOME_TIMELINE_QUERY_ID = 'c-CzHF1LboFilMpsx4ZCrQ' ;
8+ const HOME_LATEST_TIMELINE_QUERY_ID = 'BKB7oi212Fi7kQtCBGE4zA' ;
9+
10+ // Endpoint config: for-you uses GET HomeTimeline, following uses POST HomeLatestTimeline
11+ const TIMELINE_ENDPOINTS : Record < string , { endpoint : string ; method : string ; fallbackQueryId : string } > = {
12+ 'for-you' : { endpoint : 'HomeTimeline' , method : 'GET' , fallbackQueryId : HOME_TIMELINE_QUERY_ID } ,
13+ following : { endpoint : 'HomeLatestTimeline' , method : 'POST' , fallbackQueryId : HOME_LATEST_TIMELINE_QUERY_ID } ,
14+ } ;
715
816const FEATURES = {
917 rweb_video_screen_enabled : false ,
@@ -53,7 +61,7 @@ interface TimelineTweet {
5361 url : string ;
5462}
5563
56- function buildHomeTimelineUrl ( count : number , cursor ?: string | null ) : string {
64+ function buildHomeTimelineUrl ( queryId : string , endpoint : string , count : number , cursor ?: string | null ) : string {
5765 const vars : Record < string , any > = {
5866 count,
5967 includePromotedContent : false ,
@@ -63,9 +71,11 @@ function buildHomeTimelineUrl(count: number, cursor?: string | null): string {
6371 } ;
6472 if ( cursor ) vars . cursor = cursor ;
6573
66- return `/i/api/graphql/${ HOME_TIMELINE_QUERY_ID } /HomeTimeline`
67- + `?variables=${ encodeURIComponent ( JSON . stringify ( vars ) ) } `
68- + `&features=${ encodeURIComponent ( JSON . stringify ( FEATURES ) ) } ` ;
74+ return (
75+ `/i/api/graphql/${ queryId } /${ endpoint } ` +
76+ `?variables=${ encodeURIComponent ( JSON . stringify ( vars ) ) } ` +
77+ `&features=${ encodeURIComponent ( JSON . stringify ( FEATURES ) ) } `
78+ ) ;
6979}
7080
7181function extractTweet ( result : any , seen : Set < string > ) : TimelineTweet | null {
@@ -97,8 +107,8 @@ function parseHomeTimeline(data: any, seen: Set<string>): { tweets: TimelineTwee
97107 const tweets : TimelineTweet [ ] = [ ] ;
98108 let nextCursor : string | null = null ;
99109
100- const instructions =
101- data ?. data ?. home ?. home_timeline_urt ?. instructions || [ ] ;
110+ // Both HomeTimeline and HomeLatestTimeline share the same response envelope
111+ const instructions = data ?. data ?. home ?. home_timeline_urt ?. instructions || [ ] ;
102112
103113 for ( const inst of instructions ) {
104114 for ( const entry of inst . entries || [ ] ) {
@@ -144,16 +154,23 @@ function parseHomeTimeline(data: any, seen: Set<string>): { tweets: TimelineTwee
144154cli ( {
145155 site : 'twitter' ,
146156 name : 'timeline' ,
147- description : 'Fetch Twitter Home Timeline ' ,
157+ description : 'Fetch Twitter timeline (for-you or following) ' ,
148158 domain : 'x.com' ,
149159 strategy : Strategy . COOKIE ,
150160 browser : true ,
151161 args : [
162+ {
163+ name : 'type' ,
164+ default : 'for-you' ,
165+ choices : [ 'for-you' , 'following' ] ,
166+ help : 'Timeline type: for-you (algorithmic) or following (chronological)' ,
167+ } ,
152168 { name : 'limit' , type : 'int' , default : 20 } ,
153169 ] ,
154170 columns : [ 'id' , 'author' , 'text' , 'likes' , 'retweets' , 'replies' , 'views' , 'created_at' , 'url' ] ,
155171 func : async ( page , kwargs ) => {
156172 const limit = kwargs . limit || 20 ;
173+ const { endpoint, method, fallbackQueryId } = TIMELINE_ENDPOINTS [ kwargs . type || 'for-you' ] ;
157174
158175 // Navigate to x.com for cookie context
159176 await page . goto ( 'https://x.com' ) ;
@@ -165,22 +182,24 @@ cli({
165182 }` ) ;
166183 if ( ! ct0 ) throw new Error ( 'Not logged into x.com (no ct0 cookie)' ) ;
167184
168- // Dynamically resolve queryId
169- const queryId = await page . evaluate ( `async () => {
185+ // Dynamically resolve queryId for the selected endpoint
186+ const resolved = await page . evaluate ( `async () => {
170187 try {
171188 const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
172189 if (ghResp.ok) {
173190 const data = await ghResp.json();
174- const entry = data['HomeTimeline '];
191+ const entry = data['${ endpoint } '];
175192 if (entry && entry.queryId) return entry.queryId;
176193 }
177194 } catch {}
178195 return null;
179- }` ) || HOME_TIMELINE_QUERY_ID ;
196+ }` ) ;
197+ // Validate queryId format to prevent injection from untrusted upstream
198+ const queryId = typeof resolved === 'string' && / ^ [ A - Z a - z 0 - 9 _ - ] + $ / . test ( resolved ) ? resolved : fallbackQueryId ;
180199
181200 // Build auth headers
182201 const headers = JSON . stringify ( {
183- ' Authorization' : `Bearer ${ decodeURIComponent ( BEARER_TOKEN ) } ` ,
202+ Authorization : `Bearer ${ decodeURIComponent ( BEARER_TOKEN ) } ` ,
184203 'X-Csrf-Token' : ct0 ,
185204 'X-Twitter-Auth-Type' : 'OAuth2Session' ,
186205 'X-Twitter-Active-User' : 'yes' ,
@@ -193,16 +212,16 @@ cli({
193212
194213 for ( let i = 0 ; i < 5 && allTweets . length < limit ; i ++ ) {
195214 const fetchCount = Math . min ( 40 , limit - allTweets . length + 5 ) ; // over-fetch slightly for promoted filtering
196- const apiUrl = buildHomeTimelineUrl ( fetchCount , cursor )
197- . replace ( HOME_TIMELINE_QUERY_ID , queryId ) ;
215+ const apiUrl = buildHomeTimelineUrl ( queryId , endpoint , fetchCount , cursor ) ;
198216
199217 const data = await page . evaluate ( `async () => {
200- const r = await fetch("${ apiUrl } ", { headers: ${ headers } , credentials: 'include' });
218+ const r = await fetch("${ apiUrl } ", { method: " ${ method } ", headers: ${ headers } , credentials: 'include' });
201219 return r.ok ? await r.json() : { error: r.status };
202220 }` ) ;
203221
204222 if ( data ?. error ) {
205- if ( allTweets . length === 0 ) throw new Error ( `HTTP ${ data . error } : Failed to fetch timeline. queryId may have expired.` ) ;
223+ if ( allTweets . length === 0 )
224+ throw new Error ( `HTTP ${ data . error } : Failed to fetch timeline. queryId may have expired.` ) ;
206225 break ;
207226 }
208227
0 commit comments