paint-brush
Twitter APIa ulertzea, zurea diseinatu ahal izatekoarabera@trekhleb
194 irakurketak Historia berria

Twitter APIa ulertzea, zurea diseinatu ahal izateko

arabera Oleksii Trekhleb22m2024/12/16
Read on Terminal Reader

Luzeegia; Irakurri

Artikulu honetan, X (Twitter) etxeko denbora-lerroa (x.com/home) APIa nola diseinatzen den eta zein planteamendu erabiltzen dituzten hainbat erronka konpontzeko aztertzen dugu.
featured image - Twitter APIa ulertzea, zurea diseinatu ahal izateko
Oleksii Trekhleb HackerNoon profile picture
0-item
1-item

Sistemaren APIa diseinatzeko orduan software-ingeniariek sarritan aukera desberdinak hartzen dituzte kontuan REST vs RPC vs GraphQL (edo bestelako planteamendu hibridoak) zeregin edo proiektu zehatz baterako egokiena den zehazteko.


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 motak/x.ts fitxategia edo artikulu honen behealdean "Eranskina: Mota guztiak leku bakarrean" atalean.

Txioen zerrenda eskuratzen

Amaiera-puntua eta eskaera/erantzunaren egitura

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 GraphQL kontsulta , non eskatutako erantzun-egitura deskriba dezakegu, lortu nahi ditugun propietate guztiak zerrendatuz:


 # 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:

  • Txioak — ikusi TimelineItem mota
  • Kurtsoreak — ikusi TimelineCursor mota
  • Elkarrizketak/iruzkinak/hariak — ikusi TimelineModule 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.

Paginazioa

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 Desplazamendu orria vs Kurtsore orria testuingururako haria.


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.

Lotutako entitate/hierarkizatuak

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.):


  • Lehenik eta behin txioen zerrenda bakarrik itzuli behar al dugu eta, ondoren, menpeko entitateak (erabiltzaileen xehetasunak adibidez) eskatu behar diren hainbat kontsultatan eskuratu behar ditugu?
  • Edo datu guztiak aldi berean itzuli behar ditugu, lehen kargaren denbora eta tamaina handituz, baina ondorengo dei guztietarako denbora aurreztuz?
    • Datuak normalizatu behar al ditugu kasu honetan kargaren tamaina murrizteko (hau da, erabiltzaile bera txio askoren egilea denean eta txio-entitate bakoitzean erabiltzailearen datuak behin eta berriz errepikatzea saihestu nahi dugunean)?
  • Edo goiko planteamenduen konbinazioa izan behar da?


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.

Sailkatzea

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 Elur maluta ID .


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 :

  • Atera denbora-zigilua :
    • Denbora-zigilua Snowflake IDa 22 biteko eskuinera desplazatuz ateratzen da (datu zentrorako, langile IDrako eta sekuentziarako beheko 22 bitak kentzeko): 1867231621095096312 → 445182709954
  • Gehitu Twitterren Garaia :
    • Twitterren garai pertsonalizatua (1288834974657) denbora-zigilu honi gehitzeak UNIX-en denbora-zigilua milisegundotan ematen du: 445182709954 + 1288834974657 → 1734017684611ms
  • Bihurtu gizakiek irakur daitekeen data batera:
    • UNIX denbora-zigilua UTC data-ordu bihurtzeak honako hau ematen du: 1734017684611ms → 2024-12-12 15:34:44.611 (UTC)


Beraz, hemen suposa dezakegu etxeko denbora-lerroan txioak kronologikoki ordenatuta daudela.

Txio egin ekintzak

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.

Txioaren xehetasun orria

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.

Txioa gustatzea

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.


Ondorioa

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.

Eranskina: Mota guztiak leku bakarrean

Erreferentzia egiteko, mota guztiak bat-batean gehitzen ari naiz hemen. Mota guztiak aurki ditzakezu bertan motak/x.ts fitxategia.


 /** * 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;