paint-brush
Pochopenie rozhrania Twitter API, aby ste si mohli navrhnúť svoj vlastnýpodľa@trekhleb
194 čítania Nová história

Pochopenie rozhrania Twitter API, aby ste si mohli navrhnúť svoj vlastný

podľa Oleksii Trekhleb22m2024/12/16
Read on Terminal Reader

Príliš dlho; Čítať

V tomto článku sa pozrieme na to, ako je navrhnuté rozhranie API pre domovskú časovú os X (Twitter) (x.com/home) a aké prístupy používajú na riešenie viacerých problémov.
featured image - Pochopenie rozhrania Twitter API, aby ste si mohli navrhnúť svoj vlastný
Oleksii Trekhleb HackerNoon profile picture
0-item
1-item

Pokiaľ ide o návrh rozhrania API systému, softvéroví inžinieri často zvažujú rôzne možnosti, ako napr REST vs RPC vs GraphQL (alebo iné hybridné prístupy) na určenie najvhodnejšieho pre konkrétnu úlohu alebo projekt.


V tomto článku skúmame, ako je navrhnuté rozhranie API domovskej časovej osi X ( Twitter ) (x.com/home) a aké prístupy používajú na riešenie nasledujúcich problémov:

  • 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 typy žiadostí a odpovedí. Pamätajte tiež, že typy sú zjednodušené a mnohé vlastnosti sú pre stručnosť vynechané.


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 POST na nasledujúci koncový bod:


 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 odpovede (nižšie sa ponoríme hlbšie do podtypov 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ť graphql adresy URL tiež naznačuje, že X používa pre svoje API verziu GraphQL.


Používam tu slovo „príchuť“, pretože samotné telo požiadavky nevyzerá ako čisté dotaz GraphQL , kde môžeme opísať požadovanú štruktúru odozvy s uvedením všetkých vlastností, ktoré chceme načítať:


 # 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 to zmes niekoľkých prístupov . 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 HomeTimeline . 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.


Môžete si tiež všimnúť, že rovnaký TimelineRequest.queryId 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 main.js a potom sa použije pri načítaní údajov z backendu. Je pre mňa ťažké pochopiť, ako sa toto queryId 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.

Je tiež zaujímavé poznamenať, že TimelineResponse neobsahuje zoznam tweetov, ale skôr zoznam inštrukcií , ako napríklad „pridať tweet na časovú os“ (pozri typ TimelineAddEntries ) alebo „ukončiť časovú os“ (pozri TimelineTerminateTimeline typ).


Samotná inštrukcia TimelineAddEntries môže tiež obsahovať rôzne typy entít:

  • 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ť TimelineRequest.variables.count nastavuje, koľko tweetov chceme načítať naraz (na stránku). Predvolená hodnota je 20. V poli TimelineAddEntries.entries 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.cursor je zodpovedný za stránkovanie založené na kurzore.


" Stránkovanie kurzorom 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 Posunuté stránkovanie vs. stránkovanie kurzorom vlákno pre kontext.


Pri prvom načítaní zoznamu tweetov je TimelineRequest.variables.cursor prázdny, pretože chceme načítať najlepšie tweety z predvoleného (pravdepodobne vopred vypočítaného) zoznamu prispôsobených tweetov.


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: DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA . 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: eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} 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 .proto a nepoznáme ani definíciu správy .proto , môžeme len predpokladať, že backend vie, ako sa pýtať na ďalšiu dávku tweetov na základe reťazca kurzora.


Parameter TimelineResponse.variables.seenTweetIds 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.

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 tweet → user , tweet → media , media → author atď.):


  • 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 TimelineTweet používal podtyp Tweet . Pozrime sa, ako to vyzerá:


 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 tweet → media a tweet → author je vložená do odpovede pri prvom hovore (žiadne následné otázky).


Tiež prepojenia User a Media s entitami Tweet 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é.


Predpokladal som, že rozhranie UserTweets API (ktoré tu nepokrývame), ktoré je zodpovedné za načítanie tweetov jedného konkrétneho používateľa, s tým bude zaobchádzať inak, ale zjavne to tak nie je. UserTweets 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ý.


Ďalším postrehom o vzťahu entít je, že entita Media má tiež prepojenie na User (autora). Nerobí to však prostredníctvom priameho vkladania entity ako entita Tweet , ale prepája sa prostredníctvom vlastnosti 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 TweetDetail (viac o ňom v sekcii „Podrobná stránka tweetu“ nižšie).


Ďalšou entitou, ktorú má každý Tweet , je FeedbackActions (tj „Odporúčať menej často“ alebo „Zobraziť menej“). Spôsob, akým sú FeedbackActions uložené v objekte odpovede, sa líši od spôsobu, akým sú uložené objekty User a Media . Zatiaľ čo entity User a Media sú súčasťou Tweet , FeedbackActions sú uložené oddelene v poli TimelineItem.content.feedbackInfo.feedbackKeys a sú prepojené pomocou ActionKey . 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 FeedbackActions by mohli byť vložené do každého tweetu rovnakým spôsobom ako Media 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í).

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ý sortIndex môže vyzerať nejako takto '1867231621095096312' . Pravdepodobne priamo zodpovedá alebo je odvodený od a 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 TimelineItem.content.feedbackInfo.feedbackKeys a sú prepojené s tweetmi prostredníctvom 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 TimelineAction.value.childKeys ). Dáva to zmysel napríklad vtedy, keď používateľ klikne na akciu „Nepáči sa mi to“ , následným krokom môže byť zobrazenie akcie „Tento príspevok nie je relevantný“ , ako spôsob vysvetlenia, prečo používateľ tweet sa mi nepáči.

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 GET na nasledujúci koncový bod:


 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 POST , ale každý detail tweetu sa získava prostredníctvom volania GET . Zdá sa nekonzistentné. Predovšetkým majte na pamäti, že podobné parametre dopytu, ako napríklad query-id , features 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.

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 TimelineModule ). Vlákno tweetu teda vyzerá veľmi podobne ako domáci kanál časovej osi, pretože zobrazuje zoznam položiek TimelineTweet . Toto vyzerá elegantne. Dobrý príklad univerzálneho a znovu použiteľného prístupu k dizajnu API.

Páči sa mi tweet

Keď sa používateľovi páči tweet, vykoná sa požiadavka POST na nasledujúci koncový bod:


 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 typy/x.ts súbor.


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