paint-brush
Kuptimi i API-së së Twitter në mënyrë që të mund të dizajnoni tuajënnga@trekhleb
194 lexime Histori e re

Kuptimi i API-së së Twitter në mënyrë që të mund të dizajnoni tuajën

nga Oleksii Trekhleb22m2024/12/16
Read on Terminal Reader

Shume gjate; Te lexosh

Në këtë artikull, ne eksplorojmë se si është projektuar API-ja kohore e X (Twitter) në shtëpi (x.com/home) dhe çfarë qasjesh përdorin për të zgjidhur sfida të shumta.
featured image - Kuptimi i API-së së Twitter në mënyrë që të mund të dizajnoni tuajën
Oleksii Trekhleb HackerNoon profile picture
0-item
1-item

Kur bëhet fjalë për dizajnimin e API-së së sistemit, inxhinierët e softuerit shpesh marrin në konsideratë opsione të ndryshme si p.sh REST vs RPC vs GraphQL (ose qasje të tjera hibride) për të përcaktuar përshtatshmërinë më të mirë për një detyrë ose projekt specifik.


Në këtë artikull, ne eksplorojmë se si është projektuar API-ja e linjës kohore të shtëpisë X ( Twitter ) (x.com/home) dhe çfarë qasjesh përdorin për të zgjidhur sfidat e mëposhtme:

  • Si të merrni listën e tweet-eve

  • Si të bëni një renditje dhe faqezim

  • Si të ktheheni entitetet hierarkike/të lidhura (tweet, përdoruesit, media)

  • Si të merrni detajet e tweet-it

  • Si të "pëlqesh" një tweet


Ne do t'i eksplorojmë këto sfida vetëm në nivelin API, duke e trajtuar zbatimin e backend-it si një kuti të zezë, pasi nuk kemi akses në vetë kodin e backend-it.


Shfaqja e kërkesave dhe përgjigjeve të sakta këtu mund të jetë e rëndë dhe e vështirë për t'u ndjekur pasi objektet e futura thellë dhe të përsëritura janë të vështira për t'u lexuar. Për ta bërë më të lehtë shikimin e strukturës së ngarkesës së ngarkesës së kërkesës/përgjigjes, unë kam bërë përpjekjen time për të "shkruar" API-në e linjës kohore kryesore në TypeScript. Pra, kur bëhet fjalë për shembujt e kërkesës/përgjigjes, unë do të përdor llojet e kërkesës dhe përgjigjes në vend të objekteve aktuale JSON. Gjithashtu, mbani mend se llojet janë thjeshtuar dhe shumë veti janë lënë jashtë për shkurtësi.


Mund të gjeni të gjitha llojet në lloje/x.ts skedar ose në fund të këtij artikulli në seksionin "Shtojca: Të gjitha llojet në një vend".

Duke marrë listën e tweet-eve

Pika përfundimtare dhe struktura e kërkesës/përgjigjes

Marrja e listës së tweet-eve për afatin kohor të shtëpisë fillon me kërkesën POST në pikën përfundimtare të mëposhtme:


 POST https://x.com/i/api/graphql/{query-id}/HomeTimeline


Këtu është një lloj trupi i thjeshtuar i kërkesës :


 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; // ... }


Dhe këtu është një lloj trupi i thjeshtuar i përgjigjes (ne do të zhytemi më thellë në nënllojet e përgjigjes më poshtë):


 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;


Është interesante të theksohet këtu, se "marrja" e të dhënave bëhet nëpërmjet "POSTing", e cila nuk është e zakonshme për API-të e ngjashme me REST, por është e zakonshme për një API të ngjashme me GraphQL. Gjithashtu, pjesa graphql e URL-së tregon se X po përdor shijen GraphQL për API-në e tyre.


Unë po përdor fjalën "shije" këtu sepse vetë trupi i kërkesës nuk duket si i pastër Pyetja e GraphQL , ku mund të përshkruajmë strukturën e kërkuar të përgjigjes, duke renditur të gjitha vetitë që duam të marrim:


 # 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 # ... } # ... } }


Supozimi këtu është se API-ja e linjës kohore kryesore nuk është një API e pastër GraphQL, por është një përzierje e disa qasjeve . Kalimi i parametrave në një kërkesë POST si kjo duket më afër thirrjes "funksionale" RPC. Por në të njëjtën kohë, duket sikur veçoritë e GraphQL mund të përdoren diku në pjesën e pasme prapa mbajtësit/kontrolluesit të pikës fundore HomeTimeline . Një përzierje si kjo mund të shkaktohet gjithashtu nga një kod i trashëguar ose një lloj migrimi i vazhdueshëm. Por përsëri, këto janë vetëm spekulimet e mia.


Mund të vëreni gjithashtu se i njëjti TimelineRequest.queryId përdoret në URL-në e API-së si dhe në trupin e kërkesës API. Ky queryId ka shumë të ngjarë të krijohet në backend, më pas futet në paketën main.js dhe më pas përdoret kur merren të dhënat nga backend. Është e vështirë për mua të kuptoj se si përdoret saktësisht ky queryId pasi pjesa e pasme e X është një kuti e zezë në rastin tonë. Por, përsëri, spekulimi këtu mund të jetë se, mund të jetë i nevojshëm për një lloj optimizimi të performancës (duke ripërdorur disa rezultate të parallogaritura të pyetjeve?), ruajtje në memorie (në lidhje me Apollo?), korrigjimin e gabimeve (bashkohen regjistrat me queryId?), ose për qëllime gjurmimi/gjurmimi.

Është gjithashtu interesante të theksohet se TimelineResponse nuk përmban një listë të cicërimave, por më tepër një listë udhëzimesh , si "shtoni një cicërimë në afatin kohor" (shih llojin TimelineAddEntries ), ose "përfundoni afatin kohor" (shihni TimelineTerminateTimeline lloji).


Vetë udhëzimi TimelineAddEntries mund të përmbajë gjithashtu lloje të ndryshme entitetesh:

  • Tweet - shikoni llojin TimelineItem
  • Kursorët — shikoni llojin e TimelineCursor
  • Bisedat / komentet / temat - shikoni llojin TimelineModule


 type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here };


Kjo është interesante nga pikëpamja e shtrirjes pasi lejon një larmi më të gjerë të asaj që mund të jepet në afatin kohor të shtëpisë pa e modifikuar shumë API-në.

Faqezim

Vetia TimelineRequest.variables.count përcakton se sa cicërima duam të marrim menjëherë (për faqe). Parazgjedhja është 20. Megjithatë, më shumë se 20 cicërima mund të kthehen në grupin TimelineAddEntries.entries . Për shembull, grupi mund të përmbajë 37 hyrje për ngarkimin e faqes së parë, sepse përfshin cicërima (29), cicërima të ngjitura (1), cicërima të promovuara (5) dhe kursorë faqeshimi (2). Nuk jam i sigurt pse ka 29 cicërima të rregullta me numrin e kërkuar prej 20.


TimelineRequest.variables.cursor është përgjegjës për pagimin e bazuar në kursor.


" Falëzimi i kursorit përdoret më shpesh për të dhëna në kohë reale për shkak të frekuencës që shtohen rekorde të reja dhe sepse kur lexoni të dhënat shpesh shihni rezultatet më të fundit në fillim. Eliminon mundësinë e kapërcimit të artikujve dhe shfaqjes së të njëjtit artikull më shumë se një herë. Në faqezim i bazuar në kursor, një tregues konstant (ose kursori) përdoret për të mbajtur gjurmët se nga ku duhet të merren artikujt e ardhshëm në grupin e të dhënave." Shihni Pagination ofset vs faqezim kursorit fill për kontekstin.


Kur marrim listën e tweet-eve për herë të parë, TimelineRequest.variables.cursor është bosh, pasi ne duam të marrim tweet-et kryesore nga lista e parazgjedhur (me shumë mundësi e llogaritur paraprakisht) e tweet-eve të personalizuara.


Sidoqoftë, në përgjigje, së bashku me të dhënat e tweet-it, backend gjithashtu kthen shënimet e kursorit. Këtu është hierarkia e llojit të përgjigjes: 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'; }; };


Çdo faqe përmban listën e tweet-eve së bashku me kursorët "lart" dhe "poshtë":


Pasi të jenë ngarkuar të dhënat e faqes, ne mund të shkojmë nga faqja aktuale në të dy drejtimet dhe të marrim ose tweet-et "e mëparshme/më të vjetra" duke përdorur kursorin "poshtë" ose tweetet "tjetër/më të reja" duke përdorur kursorin "lart". Supozimi im është se marrja e tweet-eve "tjetër" duke përdorur kursorin "lart" ndodh në dy raste: kur tweet-et e reja u shtuan ndërsa përdoruesi është ende duke lexuar faqen aktuale, ose kur përdoruesi fillon të lëvizë furnizimin lart (dhe ka nuk ka hyrje të memories ose nëse hyrjet e mëparshme janë fshirë për arsye të performancës).


Vetë kursori i X mund të duket kështu: DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA . Në disa dizajne API, kursori mund të jetë një varg i koduar Base64 që përmban ID-në e hyrjes së fundit në listë ose vulën kohore të hyrjes së fundit të parë. Për shembull: eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} , dhe më pas, këto të dhëna përdoren për të pyetur bazën e të dhënave në përputhje me rrethanat. Në rastin e X API, duket sikur kursori është duke u deshifruar Base64 në një sekuencë binare të personalizuar që mund të kërkojë një dekodim të mëtejshëm për të nxjerrë ndonjë kuptim prej tij (dmth. nëpërmjet përkufizimeve të mesazhit Protobuf). Meqenëse nuk e dimë nëse është një kodim .proto dhe gjithashtu nuk e dimë përkufizimin e mesazhit .proto , thjesht mund të supozojmë se backend-i di se si të kërkojë grupin tjetër të cicërimave bazuar në vargun e kursorit.


Parametri TimelineResponse.variables.seenTweetIds përdoret për të informuar serverin se cilat cicërima nga faqja aktualisht aktive e lëvizjes së pafundme i ka parë klienti. Kjo me siguri ndihmon që serveri të mos përfshijë cicërima të kopjuara në faqet e mëvonshme të rezultateve.

Subjektet e lidhura/hierarkike

Një nga sfidat për t'u zgjidhur në API-të si afati kohor i shtëpisë (ose Furnizimi në shtëpi) është të kuptosh se si të kthehen entitetet e lidhura ose hierarkike (p.sh. tweet → user , tweet → media , media → author , etj):


  • A duhet të kthejmë vetëm listën e tweet-eve në fillim dhe më pas të marrim entitetet e varura (si detajet e përdoruesit) në një grup pyetjesh të veçanta sipas kërkesës?
  • Apo duhet t'i kthejmë të gjitha të dhënat menjëherë, duke rritur kohën dhe madhësinë e ngarkesës së parë, por duke kursyer kohën për të gjitha thirrjet pasuese?
    • A duhet të normalizojmë të dhënat në këtë rast për të zvogëluar madhësinë e ngarkesës (dmth. kur i njëjti përdorues është autor i shumë cicërimave dhe duam të shmangim përsëritjen e të dhënave të përdoruesit pa pushim në çdo entitet të tweet-it)?
  • Apo duhet të jetë një kombinim i qasjeve të mësipërme?


Le të shohim se si X e trajton atë.

Më parë në tipin TimelineTweet është përdorur nëntipi Tweet . Le të shohim se si duket:


 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[]; };


Ajo që është interesante këtu është se shumica e të dhënave të varura si tweet → media dhe tweet → author janë të ngulitura në përgjigjen në thirrjen e parë (pa pyetje të mëvonshme).


Gjithashtu, lidhjet e User dhe Media me entitetet Tweet nuk janë normalizuar (nëse dy tweet kanë të njëjtin autor, të dhënat e tyre do të përsëriten në çdo objekt tweet). Por duket se duhet të jetë në rregull, pasi në shtrirjen e afatit kohor të shtëpisë për një përdorues specifik, tweet-et do të jenë autore të shumë autorëve dhe përsëritjet janë të mundshme, por të pakta.


Supozimi im ishte se UserTweets API (që ne nuk e mbulojmë këtu), i cili është përgjegjës për marrjen e tweet-eve të një përdoruesi të caktuar do ta trajtojë atë ndryshe, por, me sa duket, nuk është kështu. UserTweets kthen listën e tweet-eve të të njëjtit përdorues dhe fut të njëjtat të dhëna përdoruesi pa pushim për çdo tweet. Është interesante. Ndoshta thjeshtësia e qasjes tejkalon disa madhësi të të dhënave (ndoshta të dhënat e përdoruesit konsiderohen mjaft të vogla në madhësi). nuk jam i sigurt.


Një tjetër vëzhgim në lidhje me marrëdhëniet e subjekteve është se entiteti Media ka gjithashtu një lidhje me User (autorin). Por ai nuk e bën atë nëpërmjet ngulitjes së drejtpërdrejtë të entitetit siç bën entiteti Tweet , por më tepër lidhet nëpërmjet pronës Media.source_user_id_str .


"Komentet" (të cilat janë gjithashtu "cicërima" për nga natyra e tyre) për çdo "tweet" në vijën kohore të shtëpisë nuk merren fare. Për të parë fillin e tweet-it, përdoruesi duhet të klikojë në tweet për të parë pamjen e detajuar të tij. Fillimi i tweet-it do të merret duke thirrur pikën përfundimtare TweetDetail (më shumë rreth tij në seksionin "Faqja e detajeve të Tweet" më poshtë).


Një entitet tjetër që ka çdo Tweet është FeedbackActions (p.sh. "Rekomandoni më rrallë" ose "Shiko më pak"). Mënyra se si ruhen FeedbackActions në objektin e përgjigjes është e ndryshme nga mënyra se si ruhen objektet e User dhe Media . Ndërsa entitetet User dhe Media janë pjesë e Tweet , FeedbackActions ruhen veçmas në grupin TimelineItem.content.feedbackInfo.feedbackKeys dhe janë të lidhura nëpërmjet ActionKey . Kjo ishte një surprizë e lehtë për mua pasi nuk duket të jetë rasti që ndonjë veprim të ripërdoret. Duket sikur një veprim përdoret vetëm për një cicërim të veçantë. Pra, duket sikur FeedbackActions mund të futen në çdo cicërimë në të njëjtën mënyrë si entitetet Media . Por këtu mund të më mungon ndonjë kompleksitet i fshehur (si fakti që çdo veprim mund të ketë veprime të fëmijëve).

Më shumë detaje rreth veprimeve janë në seksionin "Veprimet e Tweet" më poshtë.

Renditja

Rendi i renditjes së hyrjeve të vijës kohore përcaktohet nga pjesa e pasme nëpërmjet vetive 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', }; };


Vetë sortIndex mund të duket diçka si kjo '1867231621095096312' . Ka të ngjarë që korrespondon drejtpërdrejt me ose rrjedh nga a ID e flokeve të borës .


Në fakt, shumica e ID-ve që shihni në përgjigje (ID-të e tweet-eve) ndjekin konventën "ID-ja e flokeve të dëborës" dhe duken si '1867231621095096312' .


Nëse kjo përdoret për të renditur entitete si tweet-et, sistemi shfrytëzon renditjen e natyrshme kronologjike të ID-ve të Flak dëbore. Tweet-et ose objektet me një vlerë më të lartë sortIndex (një vulë kohore më e fundit) shfaqen më të larta në furnizim, ndërsa ato me vlera më të ulëta (një vulë kohore më e vjetër) shfaqen më të ulëta në furnizim.


Këtu është deshifrimi hap pas hapi i ID-së së Snowflake (në rastin tonë sortIndex ) 1867231621095096312 :

  • Ekstraktoni vulën kohore :
    • Vula kohore rrjedh duke zhvendosur ID-në e Snowflake me 22 bit djathtas (për të hequr 22 bitët e poshtëm për qendrën e të dhënave, ID-në e punonjësit dhe sekuencën): 1867231621095096312 → 445182709954
  • Shto epokën e Twitter :
    • Shtimi i epokës së personalizuar të Twitter (1288834974657) në këtë vulë kohore jep vulën kohore UNIX në milisekonda: 445182709954 + 1288834974657 → 1734017684611ms
  • Konverto në një datë të lexueshme nga njeriu :
    • Konvertimi i vulës kohore UNIX në një datë UTC jep: 1734017684611ms → 2024-12-12 15:34:44.611 (UTC)


Pra, këtu mund të supozojmë se tweet-et në vijën kohore kryesore janë renditur në mënyrë kronologjike.

Veprimet e cicërimave

Çdo tweet ka një menu "Veprimet".


Veprimet për çdo cicërim vijnë nga pjesa e pasme në një grup TimelineItem.content.feedbackInfo.feedbackKeys dhe janë të lidhura me tweet-et nëpërmjet 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' }; };


Është interesante këtu që ky grup i sheshtë veprimesh është në fakt një pemë (apo një grafik? Nuk e kontrollova), pasi çdo veprim mund të ketë veprime fëmijësh (shih grupin TimelineAction.value.childKeys ). Kjo ka kuptim, për shembull, kur pasi përdoruesi klikon në veprimin "Mos më pëlqen" , vazhdimi mund të jetë shfaqja e veprimit "Ky postim nuk është i rëndësishëm" , si një mënyrë për të shpjeguar pse përdoruesi nuk nuk më pëlqen tweet-i.

Faqja e detajeve të cicërimave

Sapo përdoruesi dëshiron të shohë faqen e detajeve të tweet-it (dmth. për të parë fillin e komenteve/tweet-eve), përdoruesi klikon në tweet dhe kërkesa GET për pikën përfundimtare të mëposhtme kryhet:


 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}


Isha kurioz këtu pse lista e tweet-eve po merret nëpërmjet thirrjes POST , por çdo detaj i tweet-it merret nëpërmjet thirrjes GET . Duket jokonsistente. Sidomos duke pasur parasysh se parametra të ngjashëm të pyetjes si query-id , features dhe të tjera këtë herë kalohen në URL dhe jo në trupin e kërkesës. Formati i përgjigjes është gjithashtu i ngjashëm dhe po ripërdor llojet nga thirrja në listë. Nuk jam i sigurt pse është kështu. Por përsëri, jam i sigurt se mund të më mungojë njëfarë kompleksiteti i sfondit këtu.

Këtu janë llojet e trupit të përgjigjes së thjeshtuar:


 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', }; };


Përgjigja është goxha e ngjashme (në llojet e saj) me përgjigjen e listës, kështu që nuk do të zgjasim shumë këtu.


Një nuancë interesante është se "komentet" (ose bisedat) e secilit tweet janë në fakt cicërima të tjera (shih llojin TimelineModule ). Pra, filli i tweet-it duket shumë i ngjashëm me furnizimin e linjës kohore në shtëpi duke treguar listën e hyrjeve TimelineTweet . Kjo duket elegante. Një shembull i mirë i një qasjeje universale dhe të ripërdorshme ndaj dizajnit API.

Pëlqimi i tweet-it

Kur një përdorues pëlqen tweet-in, po kryhet kërkesa POST në pikën përfundimtare të mëposhtme:


 POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet


Këtu janë llojet e trupit të kërkesës :


 type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N61twFgted2EgXILM7A' };


Këtu janë llojet e trupit të përgjigjes :


 type FavoriteTweetResponse = { data: { favorite_tweet: 'Done', } }


Duket e drejtpërdrejtë dhe gjithashtu i ngjan qasjes së ngjashme me RPC në modelin API.


konkluzioni

Ne kemi prekur disa pjesë themelore të dizajnit të API-së së linjës kohore të shtëpisë duke parë shembullin e API-së së X. Unë bëra disa supozime gjatë rrugës me aq sa dija. Besoj se disa gjëra mund t'i kem interpretuar gabimisht dhe mund të kem humbur disa nuanca komplekse. Por edhe me këtë në mendje, shpresoj të keni marrë disa njohuri të dobishme nga kjo pasqyrë e nivelit të lartë, diçka që mund ta aplikoni në sesionin tuaj të ardhshëm të Dizajnit API.


Fillimisht, kisha një plan për të kaluar nëpër uebsajte të ngjashme të teknologjisë së lartë për të marrë disa njohuri nga Facebook, Reddit, YouTube dhe të tjerët dhe për të mbledhur praktikat dhe zgjidhjet më të mira të testuara nga beteja. Nuk jam i sigurt nëse do të gjej kohë për ta bërë këtë. Do të shohë. Por mund të jetë një ushtrim interesant.

Shtojca: Të gjitha llojet në një vend

Për referencë, unë jam duke shtuar të gjitha llojet me një lëvizje këtu. Ju gjithashtu mund të gjeni të gjitha llojet në lloje/x.ts dosje.


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