Sistemaren APIa diseinatzeko orduan software-ingeniariek sarritan aukera desberdinak hartzen dituzte kontuan
Artikulu honetan, X ( Twitter ) etxeko denbora-lerroa (x.com/home) APIa nola diseinatzen den eta hurrengo erronkak konpontzeko zer ikuspegi erabiltzen dituzten aztertzen dugu:
Nola lortu txioen zerrenda
Nola egin ordenatu eta orri bat
Nola itzuli entitate hierarkikoak/lokatutakoak (txioak, erabiltzaileak, komunikabideak)
Nola lortu tweeten xehetasunak
Nola "atsegin dut" txio bat
Erronka hauek API mailan bakarrik aztertuko ditugu, backendaren inplementazioa kutxa beltz gisa tratatuz, ez baitugu backend kodea bera atzitu.
Eskaera eta erantzun zehatzak hemen erakustea astuna eta jarraitzea zaila izan daiteke, sakonki habiaratu eta errepikatzen diren objektuak irakurtzen zailak baitira. Eskaera/erantzun kargaren egitura errazago ikusteko, etxeko denbora-lerroaren APIa TypeScript-en "idazteko" saiakera egin dut. Beraz, eskaera/erantzun adibideei dagokienez, eskaera eta erantzun motak erabiliko ditut benetako JSON objektuen ordez. Gainera, gogoratu motak sinplifikatu egiten direla eta propietate asko kentzen direla laburtasunerako.
Mota guztiak aurki ditzakezu
Etxeko denbora-lerrorako txioen zerrenda eskuratzea POST
eskaerarekin hasten da amaiera-puntu honetara:
POST https://x.com/i/api/graphql/{query-id}/HomeTimeline
Hona hemen eskaera gorputz mota sinplifikatua:
type TimelineRequest = { queryId: string; // 's6ERr1UxkxxBx4YundNsXw' variables: { count: number; // 20 cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA' seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530659'] }; features: Features; }; type Features = { articles_preview_enabled: boolean; view_counts_everywhere_api_enabled: boolean; // ... }
Eta hona hemen erantzunaren gorputz mota sinplifikatua (behean erantzunen azpimotetan sakonduko dugu):
type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; responseObjects: { feedbackActions: TimelineAction[]; }; }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; }; type TimelineItem = { entryId: string; // 'tweet-1867041249938530657' sortIndex: string; // '1866561576636152411' content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; // ['-1378668161'] }; }; }; type TimelineTweet = { __typename: 'TimelineTweet'; tweet_results: { result: Tweet; }; }; type TimelineCursor = { entryId: string; // 'cursor-top-1867041249938530657' sortIndex: string; // '1866961576813152212' content: { __typename: 'TimelineTimelineCursor'; value: string; // 'DACBCgABGedb4VyaJwuKbIIZ40cX3dYwGgaAAwAEAEEAA' cursorType: 'Top' | 'Bottom'; }; }; type ActionKey = string;
Interesgarria da hemen, datuak "lortzea" "POSTing" bidez egiten dela, eta hori ez da ohikoa REST antzeko APIrako baina ohikoa da GraphQL antzeko API baterako. Gainera, URLaren graphql
zatiak adierazten du X GraphQL zaporea erabiltzen ari dela bere APIrako.
Hemen "zapore" hitza erabiltzen ari naiz, eskaeraren gorputzak berak ez duelako hutsaren itxura
# An example of a pure GraphQL request structure that is *not* being used in the X API. { tweets { id description created_at medias { kind url # ... } author { id name # ... } # ... } }
Honen suposizioa da etxeko denbora-lerroaren APIa ez dela GraphQL API hutsa, baizik eta hainbat ikuspegiren nahasketa bat dela. Honelako POST eskaera batean parametroak pasatzeak RPC dei "funtzionaletik" hurbilago dirudi. Baina, aldi berean, badirudi GraphQL funtzioak HomeTimeline amaierako kudeatzailearen/kontrolatzailearen atzean backend-en nonbait erabil daitezkeela. Baliteke horrelako nahasketa bat ondare-kode batek edo etengabeko migrazioren batek ere eragin dezake. Baina berriro ere, hauek nire espekulazioak baino ez dira.
TimelineRequest.queryId
bera APIaren URLan eta API eskaeraren gorputzean ere erabiltzen dela ohartuko zara. Seguruenik, queryId hau backend-ean sortzen da, gero main.js
sortan txertatzen da eta, ondoren, backend-etik datuak eskuratzean erabiltzen da. Zaila egiten zait ulertzea queryId
hau zehazki nola erabiltzen den X-en backend-a gure kasuan kutxa beltz bat baita. Baina, berriro ere, hemen espekulazioa izan liteke errendimenduaren optimizazio moduko bat (aldez aurretik kalkulatutako kontsulta-emaitza batzuk berrerabiliz?), cachean gordetzea (Apollorekin erlazionatuta?), arazketa (erregistroak batu queryId-en bidez?), beharrezkoa izan daitekeela. edo jarraipena/trazamendu helburuak.
Interesgarria da ohartzea ere TimelineResponse
ez duela txioen zerrenda bat, argibide -zerrenda bat baizik, "gehitu txio bat denbora-lerroan" (ikusi TimelineAddEntries
mota) edo "amaitu denbora-lerroa" (ikusi TimelineTerminateTimeline
mota).
TimelineAddEntries
instrukzioak berak ere entitate mota desberdinak izan ditzake:
TimelineItem
motaTimelineCursor
motaTimelineModule
mota
type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here };
Hau hedagarritasunaren ikuspuntutik interesgarria da, etxeko denbora-lerroan errendatu daitekeen askotariko aukera ematen baitu APIa gehiegi aldatu gabe.
TimelineRequest.variables.count
propietateak aldi berean zenbat txio lortu nahi ditugun ezartzen du (orri bakoitzeko). Lehenetsia 20 da. Hala ere, 20 txio baino gehiago itzul daitezke TimelineAddEntries.entries
matrizean. Esate baterako, matrizeak 37 sarrera izan ditzake lehen orrialdea kargatzeko, txioak (29), ainguratutako txioak (1), sustatutako txioak (5) eta orri-kurtsoreak (2) barne dituelako. Ez nago ziur zergatik dauden 29 txio erregular eskatutako 20 zenbakiarekin.
TimelineRequest.variables.cursor
da kurtsorean oinarritutako orrialdearen arduraduna.
" Kurtsorearen orriketa denbora errealeko datuetarako erabiltzen da gehienetan, erregistro berriak gehitzen diren maiztasunagatik eta datuak irakurtzerakoan sarritan azken emaitzak ikusten dituzulako lehenik. Elementuak saltatzeko eta elementu bera behin baino gehiagotan bistaratzeko aukera ezabatzen du. kurtsorean oinarritutako orriketa, erakuslea (edo kurtsorea) konstante bat erabiltzen da datu multzoan hurrengo elementuak nondik atera behar diren jakiteko." Ikusi
Txioen zerrenda lehen aldiz eskuratzean TimelineRequest.variables.cursor
hutsik dago, txio pertsonalizatuen zerrenda lehenetsitik (seguruenik aldez aurretik kalkulatutako) lehen txioak eskuratu nahi baititugu.
Hala ere, erantzunean, txioaren datuekin batera, backendak kurtsorearen sarrerak ere itzultzen ditu. Hona hemen erantzun motaren hierarkia: TimelineResponse → TimelineAddEntries → TimelineCursor
:
type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here (tweets + cursors) }; type TimelineCursor = { entryId: string; sortIndex: string; content: { __typename: 'TimelineTimelineCursor'; value: string; // 'DACBCgABGedb4VyaJwuKbIIZ40cX3dYwGgaAAwAEAEEAA' <-- Here cursorType: 'Top' | 'Bottom'; }; };
Orrialde bakoitzak txioen zerrenda dauka "goiko" eta "beheko" kurtsoreekin batera:
Orriaren datuak kargatu ondoren, uneko orrialdetik joan gaitezke bi noranzkoetan eta "aurreko/zaharra" txioak "beheko" kurtsorea erabiliz edo "hurrengo/berriagoa" txioak "goiko" kurtsorea erabiliz lor ditzakegu. Nire ustez, "hurrengo" txioak eskuratzea "goiko" kurtsorea erabiliz bi kasutan gertatzen da: erabiltzailea uneko orrialdea irakurtzen ari den bitartean txio berriak gehitzen direnean, edo erabiltzailea jarioa gorantz mugitzen hasten denean (eta daude. cacheko sarrerarik ez edo aurreko sarrerak errendimendu arrazoiengatik ezabatu badira).
X-ren kurtsorea bera honelakoa izan daiteke: DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA
. API diseinu batzuetan, kurtsorea Base64 kodetutako kate bat izan daiteke, zerrendako azken sarreraren IDa edo ikusitako azken sarreraren denbora-zigilua duena. Adibidez: eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"}
, eta, ondoren, datu hauek datu-baseari horren arabera kontsultatzeko erabiltzen dira. X API-ren kasuan, badirudi kurtsorea Base64 sekuentzia bitar pertsonalizatu batean deskodetzen ari dela, eta horrek deskodeketa gehiago behar izan dezake horren esanahia ateratzeko (hau da, Protobuf mezuen definizioen bidez). Ez dakigunez .proto
kodeketa den eta, gainera, .proto
mezuaren definizioa ezagutzen ez dugunez, pentsa dezakegu backend-ak badakiela hurrengo txio-sorta kurtsorearen katearen arabera kontsultatzen.
TimelineResponse.variables.seenTweetIds
parametroa bezeroak lehendik ikusi duen korritze infinituaren uneko orrialde aktiboko zein txioren berri emateko erabiltzen da zerbitzariari. Horrek ziurrenik zerbitzariak hurrengo emaitzen orrialdeetan bikoiztutako txioak ez dituela ziurtatzen laguntzen du.
Home timeline (edo Home Feed) bezalako APIetan konpondu beharreko erronketako bat estekatutako edo hierarkiko entitateak nola itzuli (hau da, tweet → user
, tweet → media
, media → author
, etab.):
Ikus dezagun X-k nola kudeatzen duen.
Lehenago TimelineTweet
motan Tweet
azpimota erabili zen. Ikus dezagun nola dagoen:
export type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here }; type TimelineItem = { entryId: string; sortIndex: string; content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; // <-- Here // ... }; }; type TimelineTweet = { __typename: 'TimelineTweet'; tweet_results: { result: Tweet; // <-- Here }; }; // A Tweet entity type Tweet = { __typename: 'Tweet'; core: { user_results: { result: User; // <-- Here (a dependent User entity) }; }; legacy: { full_text: string; // ... entities: { // <-- Here (a dependent Media entities) media: Media[]; hashtags: Hashtag[]; urls: Url[]; user_mentions: UserMention[]; }; }; }; // A User entity type User = { __typename: 'User'; id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2' // ... legacy: { location: string; // 'San Francisco' name: string; // 'John Doe' // ... }; }; // A Media entity type Media = { // ... source_user_id_str: string; // '1867041249938530657' <-- Here (the dependant user is being mentioned by its ID) url: string; // 'https://t.co/X78dBgtrsNU' features: { large: { faces: FaceGeometry[] }; medium: { faces: FaceGeometry[] }; small: { faces: FaceGeometry[] }; orig: { faces: FaceGeometry[] }; }; sizes: { large: MediaSize; medium: MediaSize; small: MediaSize; thumb: MediaSize; }; video_info: VideoInfo[]; };
Hemen interesgarria dena zera da: tweet → media
eta tweet → author
bezalako menpeko datu gehienak lehen deian erantzunean txertatzen direla (ondorengo kontsultarik ez).
Era berean, Tweet
entitateekin User
eta Media
konexioak ez daude normalizatuta (bi txio egile bera badute, haien datuak errepikatuko dira txio-objektu bakoitzean). Baina badirudi ondo egon beharko litzatekeela, izan ere, erabiltzaile zehatz baten etxeko denbora-lerroaren esparruan txioak egile askoren egileak izango dira eta errepikapenak posibleak dira baina urriak.
Nire ustea zen UserTweets
APIak (hemen lantzen ez duguna), erabiltzaile jakin baten txioak eskuratzeaz arduratzen dena, modu ezberdinean kudeatuko duela, baina, itxuraz, ez da horrela. UserTweets
ek erabiltzaile beraren txioen zerrenda itzultzen du eta erabiltzaile-datu berdinak txertatzen ditu behin eta berriro txio bakoitzeko. Interesgarria da. Agian, ikuspegiaren sinpletasunak datuen tamaina gainditzen du (agian erabiltzaileen datuak nahiko txikiak dira). Ez nago ziur.
Entitateen harremanari buruzko beste ohar bat da Media
entitateak User
(egilearekin) esteka bat ere baduela. Baina ez du entitate zuzeneko txertatze bidez egiten Tweet
entitateak egiten duen bezala, baizik eta Media.source_user_id_str
propietatearen bidez lotzen du.
Etxeko denbora-lerroan dauden "txio" bakoitzeko "iruzkinak" (bere izaeragatik "txioak" ere badira) ez dira batere lortzen. Txioaren haria ikusteko erabiltzaileak txioan klik egin behar du bere ikuspegi zehatza ikusteko. Txioaren haria TweetDetail
amaierako puntura deituz eskuratuko da (horri buruzko informazio gehiago beheko "Txioaren xehetasun-orria" atalean).
Tweet
bakoitzak duen beste entitate bat FeedbackActions
da (hau da, "Gutxiagotan gomendatu" edo "Ikusi gutxiago"). FeedbackActions
erantzun-objektuan gordetzeko modua User
eta Media
objektuak biltegiratzeko modua desberdina da. User
eta Media
entitateak Tweet
parte diren bitartean, FeedbackActions
bereizita gordetzen dira TimelineItem.content.feedbackInfo.feedbackKeys
array-n eta ActionKey
bidez lotzen dira. Hori sorpresa txikia izan zen niretzat, ez dirudielako edozein ekintza berrerabilgarria denik. Ekintza bat txio jakin baterako bakarrik erabiltzen dela dirudi. Beraz, badirudi FeedbackActions
txio bakoitzean txerta daitezkeela Media
entitateen modu berean. Baina agian ezkutuko konplexutasun bat faltako zait hemen (ekintza bakoitzak haurren ekintzak izan ditzakeela esate baterako).
Ekintzei buruzko xehetasun gehiago beheko "Txio-ekintzak" atalean daude.
Denbora-lerroko sarreren ordenatzeko ordena backend-ek definitzen du sortIndex
propietateen bidez:
type TimelineCursor = { entryId: string; sortIndex: string; // '1866961576813152212' <-- Here content: { __typename: 'TimelineTimelineCursor'; value: string; cursorType: 'Top' | 'Bottom'; }; }; type TimelineItem = { entryId: string; sortIndex: string; // '1866561576636152411' <-- Here content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; }; }; }; type TimelineModule = { entryId: string; sortIndex: string; // '73343543020642838441' <-- Here content: { __typename: 'TimelineTimelineModule'; items: { entryId: string, item: TimelineTweet, }[], displayType: 'VerticalConversation', }; };
sortIndex
berak '1867231621095096312'
antzeko zerbait izan dezake. Litekeena da a-ri zuzenean dagokio edo eratorria da
Egia esan, erantzunean ikusten dituzun ID gehienek (txioen IDak) "Snowflake ID" konbentzioa jarraitzen dute eta '1867231621095096312'
itxura dute.
Hau txioak bezalako entitateak ordenatzeko erabiltzen bada, sistemak Snowflake IDen berezko ordenazio kronologikoa baliatzen du. SortIndex balio handiagoa duten txioak edo objektuak (denbora-zigilu berriagoa) gorago agertzen dira jarioan, eta balio baxuagoak dituztenak (denbora-zigilu zaharragoak) jarioan baxuago agertzen dira.
Hona hemen Snowflake IDaren pausoz pauso deskodetzea (gure kasuan sortIndex
) 1867231621095096312
:
1867231621095096312 → 445182709954
445182709954 + 1288834974657 → 1734017684611ms
1734017684611ms → 2024-12-12 15:34:44.611 (UTC)
Beraz, hemen suposa dezakegu etxeko denbora-lerroan txioak kronologikoki ordenatuta daudela.
Txio bakoitzak "Ekintzak" menu bat dauka.
Txio bakoitzaren ekintzak atzealdetik datoz TimelineItem.content.feedbackInfo.feedbackKeys
array batean eta txioekin lotzen dira ActionKey
bidez:
type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; responseObjects: { feedbackActions: TimelineAction[]; // <-- Here }; }; }; }; }; type TimelineItem = { entryId: string; sortIndex: string; content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; // ['-1378668161'] <-- Here }; }; }; type TimelineAction = { key: ActionKey; // '-609233128' value: { feedbackType: 'NotRelevant' | 'DontLike' | 'SeeFewer'; // ... prompt: string; // 'This post isn't relevant' | 'Not interested in this post' | ... confirmation: string; // 'Thanks. You'll see fewer posts like this.' childKeys: ActionKey[]; // ['1192182653', '-1427553257'], ie NotInterested -> SeeFewer feedbackUrl: string; // '/2/timeline/feedback.json?feedback_type=NotRelevant&action_metadata=SRwW6oXZadPHiOczBBaAwPanEwE%3D' hasUndoAction: boolean; icon: string; // 'Frown' }; };
Interesgarria da hemen ekintza-matrize lau hau zuhaitz bat dela (edo grafiko bat? Ez dut egiaztatu), ekintza bakoitzak ekintza seme-alaba izan ditzakeelako (ikus TimelineAction.value.childKeys
array). Honek zentzuzkoa du, adibidez, erabiltzaileak "Ez gustatu" ekintza sakatu ondoren, jarraipena izan daiteke "Argitalpen hau ez da garrantzitsua" ekintza erakustea, erabiltzaileak zergatik ez duen azaltzeko modu gisa. ez zait gustatzen txioa.
Erabiltzaileak txioaren xehetasunen orria ikusi nahi duenean (hau da, iruzkinen/txioen haria ikusteko), erabiltzaileak txioan klik egiten du eta amaierako puntu honetara GET
eskaera egiten da:
GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867231621095096312","referrer":"home","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&features={"articles_preview_enabled":true}
Hemen jakin-mina nuen txioen zerrenda POST
deiaren bidez lortzen ari den, baina txioen xehetasun bakoitza GET
deiaren bidez lortzen da. Inkoherentea dirudi. Batez ere, kontuan izanda antzeko kontsulta-parametroak, hala nola, query-id
, features
, eta beste oraingoan URLan pasatzen direla eta ez eskaeraren gorputzean. Erantzun formatua ere antzekoa da eta zerrenda-deiko motak berrerabiltzen ditu. Ez nago ziur zergatik den hori. Baina berriro ere, ziur nago agian atzeko planoko konplexutasunen bat faltako zaidala hemen.
Hona hemen erantzun sinplifikatutako gorputz motak:
type TweetDetailResponse = { data: { threaded_conversation_with_injections_v2: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[], }, }, } type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; }; type TimelineTerminateTimeline = { type: 'TimelineTerminateTimeline', direction: 'Top', } type TimelineModule = { entryId: string; // 'conversationthread-58668734545929871193' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineModule'; items: { entryId: string, // 'conversationthread-1866876425669871193-tweet-1866876038930951193' item: TimelineTweet, }[], // Comments to the tweets are also tweets displayType: 'VerticalConversation', }; };
Erantzuna nahiko antzekoa da (bere motetan) zerrendako erantzunarena, beraz, ez dugu hemen denbora luzez egingo.
Ñabardura interesgarri bat da txio bakoitzaren "iruzkinak" (edo elkarrizketak) beste txio batzuk direla (ikus TimelineModule
mota). Beraz, txioaren hariak etxeko denbora-lerroaren jarioaren oso antzekoa da TimelineTweet
sarreren zerrenda erakutsiz. Hau dotorea dirudi. API diseinuaren ikuspegi unibertsal eta berrerabilgarri baten adibide ona.
Erabiltzaile bati txioa gustatzen zaionean, amaierako puntu honetara POST
eskaera egiten ari da:
POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet
Hona hemen eskaeraren gorputz motak:
type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N61twFgted2EgXILM7A' };
Hona hemen erantzunaren gorputz motak:
type FavoriteTweetResponse = { data: { favorite_tweet: 'Done', } }
Itxura zuzena du eta APIaren diseinurako RPC antzeko ikuspegiaren antza du.
Etxeko denbora-lerroaren APIaren diseinuaren oinarrizko atal batzuk ukitu ditugu X-ren APIaren adibidea ikusiz. Bidean hipotesi batzuk egin nituen nire jakinaren arabera. Uste dut gaizki interpretatu nituzkeen gauza batzuk eta agian ñabardura konplexu batzuk galduko nituzkeela. Baina hori kontuan izanda ere, goi-mailako ikuspegi orokor honetatik informazio baliagarri batzuk lortu dituzula espero dut, zure hurrengo API Diseinu saioan aplikatu dezakezun zerbait.
Hasieran, goi-mailako teknologiaren antzeko webguneetatik pasatzeko plana nuen Facebook, Reddit, YouTube eta beste batzuen informazio batzuk lortzeko eta borrokan probatutako praktika onak eta irtenbideak biltzeko. Ez nago ziur hori egiteko astirik aurkituko dudan. Ikusiko da. Baina ariketa interesgarria izan daiteke.
Erreferentzia egiteko, mota guztiak bat-batean gehitzen ari naiz hemen. Mota guztiak aurki ditzakezu bertan
/** * This file contains the simplified types for X's (Twitter's) home timeline API. * * These types are created for exploratory purposes, to see the current implementation * of the X's API, to see how they fetch Home Feed, how they do a pagination and sorting, * and how they pass the hierarchical entities (posts, media, user info, etc). * * Many properties and types are omitted for simplicity. */ // POST https://x.com/i/api/graphql/{query-id}/HomeTimeline export type TimelineRequest = { queryId: string; // 's6ERr1UxkxxBx4YundNsXw' variables: { count: number; // 20 cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA' seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530658'] }; features: Features; }; // POST https://x.com/i/api/graphql/{query-id}/HomeTimeline export type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; responseObjects: { feedbackActions: TimelineAction[]; }; }; }; }; }; // POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet export type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N6OtwFgted2EgXILM7A' }; // POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet export type FavoriteTweetResponse = { data: { favorite_tweet: 'Done', } } // GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867041249938530657","referrer":"home","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&features={"articles_preview_enabled":true} export type TweetDetailResponse = { data: { threaded_conversation_with_injections_v2: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[], }, }, } type Features = { articles_preview_enabled: boolean; view_counts_everywhere_api_enabled: boolean; // ... } type TimelineAction = { key: ActionKey; // '-609233128' value: { feedbackType: 'NotRelevant' | 'DontLike' | 'SeeFewer'; // ... prompt: string; // 'This post isn't relevant' | 'Not interested in this post' | ... confirmation: string; // 'Thanks. You'll see fewer posts like this.' childKeys: ActionKey[]; // ['1192182653', '-1427553257'], ie NotInterested -> SeeFewer feedbackUrl: string; // '/2/timeline/feedback.json?feedback_type=NotRelevant&action_metadata=SRwW6oXZadPHiOczBBaAwPanEwE%3D' hasUndoAction: boolean; icon: string; // 'Frown' }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; }; type TimelineTerminateTimeline = { type: 'TimelineTerminateTimeline', direction: 'Top', } type TimelineCursor = { entryId: string; // 'cursor-top-1867041249938530657' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineCursor'; value: string; // 'DACBCgABGedb4VyaJwuKbIIZ40cX3dYwGgaAAwAEAEEAA' cursorType: 'Top' | 'Bottom'; }; }; type TimelineItem = { entryId: string; // 'tweet-1867041249938530657' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; // ['-1378668161'] }; }; }; type TimelineModule = { entryId: string; // 'conversationthread-1867041249938530657' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineModule'; items: { entryId: string, // 'conversationthread-1867041249938530657-tweet-1867041249938530657' item: TimelineTweet, }[], // Comments to the tweets are also tweets displayType: 'VerticalConversation', }; }; type TimelineTweet = { __typename: 'TimelineTweet'; tweet_results: { result: Tweet; }; }; type Tweet = { __typename: 'Tweet'; core: { user_results: { result: User; }; }; views: { count: string; // '13763' }; legacy: { bookmark_count: number; // 358 created_at: string; // 'Tue Dec 10 17:41:28 +0000 2024' conversation_id_str: string; // '1867041249938530657' display_text_range: number[]; // [0, 58] favorite_count: number; // 151 full_text: string; // "How I'd promote my startup, if I had 0 followers (Part 1)" lang: string; // 'en' quote_count: number; reply_count: number; retweet_count: number; user_id_str: string; // '1867041249938530657' id_str: string; // '1867041249938530657' entities: { media: Media[]; hashtags: Hashtag[]; urls: Url[]; user_mentions: UserMention[]; }; }; }; type User = { __typename: 'User'; id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2' rest_id: string; // '1867041249938530657' is_blue_verified: boolean; profile_image_shape: 'Circle'; // ... legacy: { following: boolean; created_at: string; // 'Thu Oct 21 09:30:37 +0000 2021' description: string; // 'I help startup founders double their MRR with outside-the-box marketing cheat sheets' favourites_count: number; // 22195 followers_count: number; // 25658 friends_count: number; location: string; // 'San Francisco' media_count: number; name: string; // 'John Doe' profile_banner_url: string; // 'https://pbs.twimg.com/profile_banners/4863509452891265813/4863509' profile_image_url_https: string; // 'https://pbs.twimg.com/profile_images/4863509452891265813/4863509_normal.jpg' screen_name: string; // 'johndoe' url: string; // 'https://t.co/dgTEddFGDd' verified: boolean; }; }; type Media = { display_url: string; // 'pic.x.com/X7823zS3sNU' expanded_url: string; // 'https://x.com/johndoe/status/1867041249938530657/video/1' ext_alt_text: string; // 'Image of two bridges.' id_str: string; // '1867041249938530657' indices: number[]; // [93, 116] media_key: string; // '13_2866509231399826944' media_url_https: string; // 'https://pbs.twimg.com/profile_images/1867041249938530657/4863509_normal.jpg' source_status_id_str: string; // '1867041249938530657' source_user_id_str: string; // '1867041249938530657' type: string; // 'video' url: string; // 'https://t.co/X78dBgtrsNU' features: { large: { faces: FaceGeometry[] }; medium: { faces: FaceGeometry[] }; small: { faces: FaceGeometry[] }; orig: { faces: FaceGeometry[] }; }; sizes: { large: MediaSize; medium: MediaSize; small: MediaSize; thumb: MediaSize; }; video_info: VideoInfo[]; }; type UserMention = { id_str: string; // '98008038' name: string; // 'Yann LeCun' screen_name: string; // 'ylecun' indices: number[]; // [115, 122] }; type Hashtag = { indices: number[]; // [257, 263] text: string; }; type Url = { display_url: string; // 'google.com' expanded_url: string; // 'http://google.com' url: string; // 'https://t.co/nZh3aF0Aw6' indices: number[]; // [102, 125] }; type VideoInfo = { aspect_ratio: number[]; // [427, 240] duration_millis: number; // 20000 variants: { bitrate?: number; // 288000 content_type?: string; // 'application/x-mpegURL' | 'video/mp4' | ... url: string; // 'https://video.twimg.com/amplify_video/18665094345456w6944/pl/-ItQau_LRWedR-W7.m3u8?tag=14' }; }; type FaceGeometry = { x: number; y: number; h: number; w: number }; type MediaSize = { h: number; w: number; resize: 'fit' | 'crop' }; type ActionKey = string;