Sistemaren APIa diseinatzeko orduan software-ingeniariek sarritan aukera desberdinak hartzen dituzte kontuan (edo bestelako planteamendu hibridoak) zeregin edo proiektu zehatz baterako egokiena den zehazteko. REST vs RPC vs GraphQL Artikulu honetan, ( ) etxeko denbora-lerroa (x.com/home) APIa nola diseinatzen den eta hurrengo erronkak konpontzeko zer ikuspegi erabiltzen dituzten aztertzen dugu: X Twitter 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 erabiliko ditut benetako JSON objektuen ordez. Gainera, gogoratu motak sinplifikatu egiten direla eta propietate asko kentzen direla laburtasunerako. motak 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 eskaerarekin hasten da amaiera-puntu honetara: POST POST https://x.com/i/api/graphql/{query-id}/HomeTimeline Hona hemen mota sinplifikatua: eskaera gorputz 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 gorputz mota sinplifikatua (behean erantzunen azpimotetan sakonduko dugu): erantzunaren 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 zatiak adierazten du X GraphQL zaporea erabiltzen ari dela bere APIrako. graphql Hemen hitza erabiltzen ari naiz, eskaeraren gorputzak berak ez duelako hutsaren itxura , non eskatutako erantzun-egitura deskriba dezakegu, lortu nahi ditugun propietate guztiak zerrendatuz: "zapore" GraphQL kontsulta # 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 bat dela. Honelako POST eskaera batean parametroak pasatzeak RPC dei "funtzionaletik" hurbilago dirudi. Baina, aldi berean, badirudi GraphQL funtzioak 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. hainbat ikuspegiren nahasketa HomeTimeline bera APIaren URLan eta API eskaeraren gorputzean ere erabiltzen dela ohartuko zara. Seguruenik, queryId hau backend-ean sortzen da, gero sortan txertatzen da eta, ondoren, backend-etik datuak eskuratzean erabiltzen da. Zaila egiten zait ulertzea 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. TimelineRequest.queryId main.js queryId Interesgarria da ohartzea ere ez duela txioen zerrenda bat, -zerrenda bat baizik, (ikusi mota) edo (ikusi mota). TimelineResponse argibide "gehitu txio bat denbora-lerroan" TimelineAddEntries "amaitu denbora-lerroa" TimelineTerminateTimeline instrukzioak berak ere entitate mota desberdinak izan ditzake: TimelineAddEntries Txioak — ikusi mota TimelineItem Kurtsoreak — ikusi mota TimelineCursor Elkarrizketak/iruzkinak/hariak — ikusi mota TimelineModule 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 propietateak aldi berean zenbat txio lortu nahi ditugun ezartzen du (orri bakoitzeko). Lehenetsia 20 da. Hala ere, 20 txio baino gehiago itzul daitezke 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.count TimelineAddEntries.entries da kurtsorean oinarritutako orrialdearen arduraduna. TimelineRequest.variables.cursor " 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 Kurtsorearen orriketa Desplazamendu orria vs Kurtsore orria testuingururako haria. Txioen zerrenda lehen aldiz eskuratzean hutsik dago, txio pertsonalizatuen zerrenda lehenetsitik (seguruenik aldez aurretik kalkulatutako) lehen txioak eskuratu nahi baititugu. TimelineRequest.variables.cursor 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: . API diseinu batzuetan, kurtsorea Base64 kodetutako kate bat izan daiteke, zerrendako azken sarreraren IDa edo ikusitako azken sarreraren denbora-zigilua duena. Adibidez: , 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 kodeketa den eta, gainera, mezuaren definizioa ezagutzen ez dugunez, pentsa dezakegu backend-ak badakiela hurrengo txio-sorta kurtsorearen katearen arabera kontsultatzen. DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} .proto .proto 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. TimelineResponse.variables.seenTweetIds Lotutako entitate/hierarkizatuak Home timeline (edo Home Feed) bezalako APIetan konpondu beharreko erronketako bat estekatutako edo hierarkiko entitateak nola itzuli (hau da, , , , etab.): tweet → user tweet → media media → author 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 motan azpimota erabili zen. Ikus dezagun nola dagoen: TimelineTweet Tweet 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: eta bezalako menpeko datu gehienak lehen deian erantzunean txertatzen direla (ondorengo kontsultarik ez). tweet → media tweet → author Era berean, entitateekin eta 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. Tweet User Media Nire ustea zen APIak (hemen lantzen ez duguna), txioak eskuratzeaz arduratzen dena, modu ezberdinean kudeatuko duela, baina, itxuraz, ez da horrela. 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. UserTweets erabiltzaile jakin baten UserTweets Entitateen harremanari buruzko beste ohar bat da entitateak (egilearekin) esteka bat ere baduela. Baina ez du entitate zuzeneko txertatze bidez egiten entitateak egiten duen bezala, baizik eta propietatearen bidez lotzen du. Media User Tweet Media.source_user_id_str 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 amaierako puntura deituz eskuratuko da (horri buruzko informazio gehiago beheko "Txioaren xehetasun-orria" atalean). TweetDetail bakoitzak duen beste entitate bat da (hau da, "Gutxiagotan gomendatu" edo "Ikusi gutxiago"). erantzun-objektuan gordetzeko modua eta objektuak biltegiratzeko modua desberdina da. eta entitateak parte diren bitartean, bereizita gordetzen dira array-n eta 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 txio bakoitzean txerta daitezkeela entitateen modu berean. Baina agian ezkutuko konplexutasun bat faltako zait hemen (ekintza bakoitzak haurren ekintzak izan ditzakeela esate baterako). Tweet FeedbackActions FeedbackActions User Media User Media Tweet FeedbackActions TimelineItem.content.feedbackInfo.feedbackKeys ActionKey FeedbackActions Media Ekintzei buruzko xehetasun gehiago beheko "Txio-ekintzak" atalean daude. Sailkatzea Denbora-lerroko sarreren ordenatzeko ordena backend-ek definitzen du propietateen bidez: sortIndex 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', }; }; berak antzeko zerbait izan dezake. Litekeena da a-ri zuzenean dagokio edo eratorria da . sortIndex '1867231621095096312' Elur maluta ID itxura dute. Egia esan, erantzunean ikusten dituzun ID gehienek (txioen IDak) "Snowflake ID" konbentzioa jarraitzen dute eta '1867231621095096312' 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 batera: gizakiek irakur daitekeen data 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 array batean eta txioekin lotzen dira bidez: TimelineItem.content.feedbackInfo.feedbackKeys ActionKey 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 array). Honek zentzuzkoa du, adibidez, erabiltzaileak ekintza sakatu ondoren, jarraipena izan daiteke ekintza erakustea, erabiltzaileak zergatik ez duen azaltzeko modu gisa. ez zait gustatzen txioa. TimelineAction.value.childKeys "Ez gustatu" "Argitalpen hau ez da garrantzitsua" 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 eskaera egiten da: GET 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 deiaren bidez lortzen ari den, baina txioen xehetasun bakoitza deiaren bidez lortzen da. Inkoherentea dirudi. Batez ere, kontuan izanda antzeko kontsulta-parametroak, hala nola, , , 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. POST GET query-id features 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 mota). Beraz, txioaren hariak etxeko denbora-lerroaren jarioaren oso antzekoa da sarreren zerrenda erakutsiz. Hau dotorea dirudi. API diseinuaren ikuspegi unibertsal eta berrerabilgarri baten adibide ona. TimelineModule TimelineTweet Txioa gustatzea Erabiltzaile bati txioa gustatzen zaionean, amaierako puntu honetara eskaera egiten ari da: POST POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet Hona hemen gorputz motak: eskaeraren type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N61twFgted2EgXILM7A' }; Hona hemen gorputz motak: erantzunaren 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 fitxategia. motak/x.ts /** * 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;