Pokiaľ ide o návrh rozhrania API systému, softvéroví inžinieri často zvažujú rôzne možnosti, ako napr (alebo iné hybridné prístupy) na určenie najvhodnejšieho pre konkrétnu úlohu alebo projekt. REST vs RPC vs GraphQL V tomto článku skúmame, ako je navrhnuté rozhranie API domovskej časovej osi ( ) (x.com/home) a aké prístupy používajú na riešenie nasledujúcich problémov: X Twitter Ako získať zoznam tweetov Ako urobiť triedenie a stránkovanie Ako vrátiť hierarchické/prepojené entity (tweety, používatelia, médiá) Ako získať podrobnosti o tweete Ako dať „páči sa mi“ tweet Tieto výzvy preskúmame iba na úrovni API, pričom implementáciu backendu budeme považovať za čiernu skrinku, keďže nemáme prístup k samotnému kódu backendu. Zobrazenie presných požiadaviek a odpovedí tu môže byť ťažkopádne a ťažko sledovateľné, pretože hlboko vnorené a opakujúce sa objekty sa ťažko čítajú. Aby som uľahčil zobrazenie štruktúry užitočného zaťaženia požiadavky/odpovede, pokúsil som sa „napísať“ rozhranie API domácej časovej osi v TypeScript. Takže pokiaľ ide o príklady žiadosti/odpovede, použijem namiesto skutočných objektov JSON žiadostí a odpovedí. Pamätajte tiež, že typy sú zjednodušené a mnohé vlastnosti sú pre stručnosť vynechané. typy Všetky typy nájdete v typy/x.ts súbor alebo v spodnej časti tohto článku v časti „Príloha: Všetky typy na jednom mieste“. Načítava sa zoznam tweetov Koncový bod a štruktúra požiadavky/odpovede Načítanie zoznamu tweetov pre domácu časovú os začína požiadavkou na nasledujúci koncový bod: POST POST https://x.com/i/api/graphql/{query-id}/HomeTimeline Tu je zjednodušený typ tela : žiadosti 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; // ... } A tu je zjednodušený typ tela (nižšie sa ponoríme hlbšie do podtypov odpovede): odpovede 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; Je zaujímavé poznamenať, že „získanie“ údajov sa vykonáva prostredníctvom „POSTingu“, čo nie je bežné pre API podobné REST, ale je bežné pre API podobné GraphQL. Časť adresy URL tiež naznačuje, že X používa pre svoje API verziu GraphQL. graphql Používam tu slovo pretože samotné telo požiadavky nevyzerá ako čisté , kde môžeme opísať požadovanú štruktúru odozvy s uvedením všetkých vlastností, ktoré chceme načítať: „príchuť“, dotaz GraphQL # 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 # ... } # ... } } Predpokladom je, že rozhranie API domácej časovej osi nie je čisté rozhranie GraphQL API, ale je . Odovzdanie parametrov v požiadavke POST sa zdá byť bližšie k "funkčnému" volaniu RPC. Zároveň sa však zdá, že funkcie GraphQL možno použiť niekde na backende za obslužným programom/ovládačom koncového bodu . Takýto mix môže byť spôsobený aj starým kódom alebo nejakým druhom prebiehajúcej migrácie. Ale opäť sú to len moje špekulácie. to zmes niekoľkých prístupov HomeTimeline Môžete si tiež všimnúť, že rovnaký sa používa v adrese URL rozhrania API, ako aj v tele žiadosti rozhrania API. Toto queryId sa s najväčšou pravdepodobnosťou vygeneruje na backende, potom sa vloží do balíka a potom sa použije pri načítaní údajov z backendu. Je pre mňa ťažké pochopiť, ako sa toto presne používa, pretože backend X je v našom prípade čierna skrinka. Ale opäť tu možno špekulovať o tom, že to môže byť potrebné pre určitý druh optimalizácie výkonu (opätovné použitie niektorých vopred vypočítaných výsledkov dotazu?), ukladanie do vyrovnávacej pamäte (súvisiace s Apollom?), ladenie (pripojenie k protokolom pomocou queryId?), alebo na účely sledovania/sledovania. TimelineRequest.queryId main.js queryId Je tiež zaujímavé poznamenať, že neobsahuje zoznam tweetov, ale skôr zoznam , ako napríklad (pozri typ ) alebo (pozri typ). TimelineResponse inštrukcií „pridať tweet na časovú os“ TimelineAddEntries „ukončiť časovú os“ TimelineTerminateTimeline Samotná inštrukcia môže tiež obsahovať rôzne typy entít: TimelineAddEntries Tweety – pozrite si typ TimelineItem Kurzory — pozrite si typ TimelineCursor Konverzácie/komentáre/vlákna — pozrite si typ TimelineModule type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here }; To je zaujímavé z hľadiska rozšíriteľnosti, pretože umožňuje širšiu škálu toho, čo je možné vykresliť na domácej časovej osi bez prílišného vylaďovania API. Stránkovanie Vlastnosť nastavuje, koľko tweetov chceme načítať naraz (na stránku). Predvolená hodnota je 20. V poli však možno vrátiť viac ako 20 tweetov. Napríklad pole môže obsahovať 37 položiek pre prvé načítanie stránky, pretože obsahuje tweety (29), pripnuté tweety (1), propagované tweety (5) a kurzory stránkovania (2). Nie som si istý, prečo existuje 29 bežných tweetov s požadovaným počtom 20. TimelineRequest.variables.count TimelineAddEntries.entries je zodpovedný za stránkovanie založené na kurzore. TimelineRequest.variables.cursor " sa najčastejšie používa pre údaje v reálnom čase kvôli frekvencii pridávania nových záznamov a preto, že pri čítaní údajov často vidíte ako prvé najnovšie výsledky. Eliminuje možnosť preskakovania položiek a zobrazenia tej istej položky viackrát. V stránkovanie založené na kurzore, konštantný ukazovateľ (alebo kurzor) sa používa na sledovanie toho, odkiaľ v množine údajov by sa mali načítať ďalšie položky." Pozrite si Stránkovanie kurzorom Posunuté stránkovanie vs. stránkovanie kurzorom vlákno pre kontext. Pri prvom načítaní zoznamu tweetov je prázdny, pretože chceme načítať najlepšie tweety z predvoleného (pravdepodobne vopred vypočítaného) zoznamu prispôsobených tweetov. TimelineRequest.variables.cursor V odpovedi však spolu s údajmi tweetu backend vráti aj položky kurzora. Tu je hierarchia typov odpovede: : 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'; }; }; Každá stránka obsahuje zoznam tweetov spolu s „horným“ a „dolným“ kurzorom: Po načítaní údajov stránky môžeme prejsť z aktuálnej stránky oboma smermi a načítať buď „predchádzajúce/staršie“ tweety pomocou „dolného“ kurzora, alebo „nasledujúce/novšie“ tweety pomocou „horného“ kurzora. Predpokladám, že načítanie „ďalších“ tweetov pomocou „horného“ kurzora sa deje v dvoch prípadoch: keď boli nové tweety pridané, keď používateľ stále číta aktuálnu stránku, alebo keď používateľ začne posúvať informačný kanál smerom nahor (a existujú žiadne záznamy vo vyrovnávacej pamäti alebo ak boli predchádzajúce záznamy vymazané z dôvodov výkonu). Samotný kurzor X môže vyzerať takto: . V niektorých návrhoch API môže byť kurzorom kódovaný reťazec Base64, ktorý obsahuje id poslednej položky v zozname alebo časovú pečiatku poslednej zobrazenej položky. Napríklad: a potom sa tieto údaje použijú na zodpovedajúci dotaz v databáze. V prípade X API to vyzerá, že kurzor sa dekóduje Base64 do nejakej vlastnej binárnej sekvencie, ktorá si môže vyžadovať ďalšie dekódovanie, aby z toho získal nejaký význam (tj prostredníctvom definícií správ Protobuf). Keďže nevieme, či ide o kódovanie a nepoznáme ani definíciu správy , môžeme len predpokladať, že backend vie, ako sa pýtať na ďalšiu dávku tweetov na základe reťazca kurzora. DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} .proto .proto Parameter sa používa na informovanie servera o tom, ktoré tweety z aktuálne aktívnej stránky nekonečného rolovania už klient videl. To s najväčšou pravdepodobnosťou pomáha zabezpečiť, aby server nezahŕňal duplicitné tweety na nasledujúcich stránkach výsledkov. TimelineResponse.variables.seenTweetIds Prepojené/hierarchické entity Jednou z výziev, ktoré treba vyriešiť v rozhraniach API, ako je domáca časová os (alebo domovský kanál), je zistiť, ako vrátiť prepojené alebo hierarchické entity (tj , , atď.): tweet → user tweet → media media → author Mali by sme najskôr vrátiť iba zoznam tweetov a potom načítať závislé entity (napríklad podrobnosti o používateľovi) v skupine samostatných dopytov na požiadanie? Alebo by sme mali vrátiť všetky údaje naraz, čím sa zvýši čas a veľkosť prvého zaťaženia, ale ušetrí sa čas pre všetky nasledujúce hovory? Potrebujeme v tomto prípade normalizovať údaje, aby sme znížili veľkosť užitočného obsahu (tj keď ten istý používateľ je autorom mnohých tweetov a chceme sa vyhnúť opakovaniu používateľských údajov v každom tweete)? Alebo by to mala byť kombinácia vyššie uvedených prístupov? Pozrime sa, ako si s tým X poradí. Predtým sa v type používal podtyp . Pozrime sa, ako to vyzerá: 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[]; }; Zaujímavé je, že väčšina závislých údajov ako a je vložená do odpovede pri prvom hovore (žiadne následné otázky). tweet → media tweet → author Tiež prepojenia a s entitami nie sú normalizované (ak majú dva tweety rovnakého autora, ich údaje sa budú opakovať v každom objekte tweetu). Zdá sa však, že by to malo byť v poriadku, keďže v rámci domovskej časovej osi pre konkrétneho používateľa budú tweety autormi mnohých autorov a opakovania sú možné, ale sú zriedkavé. User Media Tweet Predpokladal som, že rozhranie API (ktoré tu nepokrývame), ktoré je zodpovedné za načítanie tweetov s tým bude zaobchádzať inak, ale zjavne to tak nie je. vráti zoznam tweetov toho istého používateľa a do každého tweetu znova a znova vloží rovnaké používateľské údaje. Je to zaujímavé. Možno, že jednoduchosť prístupu prekonáva určitú réžiu veľkosti údajov (možno sa používateľské údaje považujú za dosť malé). nie som si istý. UserTweets jedného konkrétneho používateľa, UserTweets Ďalším postrehom o vzťahu entít je, že entita má tiež prepojenie na (autora). Nerobí to však prostredníctvom priameho vkladania entity ako entita , ale prepája sa prostredníctvom vlastnosti . Media User Tweet Media.source_user_id_str „Komentáre“ (ktoré sú svojou povahou tiež „tweety“) pre každý „tweet“ na domovskej časovej osi sa vôbec nenačítajú. Na zobrazenie vlákna tweetu musí používateľ kliknúť na tweet, aby sa mu zobrazilo jeho podrobné zobrazenie. Vlákno tweetu sa načíta volaním koncového bodu (viac o ňom v sekcii „Podrobná stránka tweetu“ nižšie). TweetDetail Ďalšou entitou, ktorú má každý , je (tj „Odporúčať menej často“ alebo „Zobraziť menej“). Spôsob, akým sú uložené v objekte odpovede, sa líši od spôsobu, akým sú uložené objekty a . Zatiaľ čo entity a sú súčasťou , sú uložené oddelene v poli a sú prepojené pomocou . To bolo pre mňa mierne prekvapenie, pretože sa nezdá, že by bola akákoľvek akcia znovu použiteľná. Zdá sa, že jedna akcia sa používa iba pre jeden konkrétny tweet. Zdá sa teda, že by mohli byť vložené do každého tweetu rovnakým spôsobom ako entity. Ale možno mi tu chýba nejaká skrytá zložitosť (napríklad skutočnosť, že každá akcia môže mať akcie detí). Tweet FeedbackActions FeedbackActions User Media User Media Tweet FeedbackActions TimelineItem.content.feedbackInfo.feedbackKeys ActionKey FeedbackActions Media Viac podrobností o akciách nájdete v sekcii „Akcie tweetov“ nižšie. Triedenie Poradie triedenia položiek časovej osi je definované koncovým serverom prostredníctvom vlastností : 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', }; }; Samotný môže vyzerať nejako takto . Pravdepodobne priamo zodpovedá alebo je odvodený od a . sortIndex '1867231621095096312' ID snehovej vločky . V skutočnosti väčšina ID, ktoré vidíte v odpovedi (ID tweetu), sa riadi konvenciou „ID snehovej vločky“ a vyzerá ako '1867231621095096312' Ak sa to používa na triedenie entít, ako sú tweety, systém využíva inherentné chronologické triedenie ID snehových vločiek. Tweety alebo objekty s vyššou hodnotou sortIndex (novšia časová pečiatka) sa v informačnom kanáli zobrazujú vyššie, zatiaľ čo objekty s nižšími hodnotami (staršia časová pečiatka) sa v informačnom kanáli zobrazujú nižšie. Tu je krok za krokom dekódovanie ID snehovej vločky (v našom prípade ) : sortIndex 1867231621095096312 Extrahujte : časovú pečiatku Časová pečiatka je odvodená posunutím ID snehovej vločky doprava o 22 bitov (na odstránenie spodných 22 bitov pre dátové centrum, ID pracovníka a sekvenciu): 1867231621095096312 → 445182709954 Pridať : epochu Twitteru Pridaním vlastnej epochy Twitteru (1288834974657) k tejto časovej pečiatke získate časovú pečiatku UNIX v milisekundách: 445182709954 + 1288834974657 → 1734017684611ms Previesť na : ľudsky čitateľný dátum Konverzia časovej pečiatky UNIX na dátum a čas UTC dáva: 1734017684611ms → 2024-12-12 15:34:44.611 (UTC) Tu teda môžeme predpokladať, že tweety v domovskej časovej osi sú zoradené chronologicky. Tweet akcie Každý tweet má ponuku „Akcie“. Akcie pre každý tweet pochádzajú z backendu v poli a sú prepojené s tweetmi prostredníctvom : 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' }; }; Je zaujímavé, že toto ploché pole akcií je vlastne strom (alebo graf? Nekontroloval som), keďže každá akcia môže mať podradené akcie (pozri pole ). Dáva to zmysel napríklad vtedy, keď používateľ klikne na akciu , následným krokom môže byť zobrazenie akcie , ako spôsob vysvetlenia, prečo používateľ tweet sa mi nepáči. TimelineAction.value.childKeys „Nepáči sa mi to“ „Tento príspevok nie je relevantný“ Stránka s podrobnosťami o tweete Keď chce používateľ zobraziť stránku s podrobnosťami o tweete (tj vidieť vlákno komentárov/tweetov), klikne na tweet a vykoná sa požiadavka na nasledujúci koncový bod: 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} Bol som zvedavý, prečo sa zoznam tweetov získava prostredníctvom volania , ale každý detail tweetu sa získava prostredníctvom volania . Zdá sa nekonzistentné. Predovšetkým majte na pamäti, že podobné parametre dopytu, ako napríklad , a ďalšie, sa tentoraz odovzdávajú v adrese URL a nie v tele žiadosti. Formát odpovede je tiež podobný a opätovne používa typy z volania zoznamu. Nie som si istý, prečo je to tak. Ale opäť som si istý, že mi tu môže chýbať určitá zložitosť pozadia. POST GET query-id features Tu sú zjednodušené typy odpovedí: 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', }; }; Odpoveď je dosť podobná (vo svojich typoch) odpovedi zoznamu, takže tu nebudeme príliš dlho. Jednou zaujímavou nuansou je, že „komentáre“ (alebo konverzácie) každého tweetu sú v skutočnosti iné tweety (pozri typ ). Vlákno tweetu teda vyzerá veľmi podobne ako domáci kanál časovej osi, pretože zobrazuje zoznam položiek . Toto vyzerá elegantne. Dobrý príklad univerzálneho a znovu použiteľného prístupu k dizajnu API. TimelineModule TimelineTweet Páči sa mi tweet Keď sa používateľovi páči tweet, vykoná sa požiadavka na nasledujúci koncový bod: POST POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet Tu sú typy tela : žiadosti type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N61twFgted2EgXILM7A' }; Tu sú typy tela : odpovede type FavoriteTweetResponse = { data: { favorite_tweet: 'Done', } } Vyzerá priamočiaro a tiež pripomína RPC prístup k dizajnu API. Záver Dotkli sme sa niektorých základných častí dizajnu API domácej časovej osi pri pohľade na príklad API X. Po ceste som urobil nejaké predpoklady, ako som najlepšie vedel. Verím, že niektoré veci som si možno nesprávne vysvetlil a možno mi ušli niektoré zložité nuansy. Ale aj s ohľadom na to dúfam, že ste z tohto prehľadu na vysokej úrovni získali užitočné informácie, ktoré by ste mohli použiť vo svojej ďalšej relácii API Design. Pôvodne som mal v pláne prejsť podobné špičkové webové stránky, aby som získal prehľad z Facebooku, Redditu, YouTube a ďalších a zhromaždil osvedčené postupy a riešenia overené v boji. Nie som si istý, či si na to nájdem čas. Uvidíme. Ale mohlo by to byť zaujímavé cvičenie. Dodatok: Všetky typy na jednom mieste Pre referenciu sem pridávam všetky typy naraz. Všetky typy nájdete aj v súbor. typy/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;