paint-brush
Հասկանալով Twitter API-ն, որպեսզի կարողանաք նախագծել ձեր սեփականըկողմից@trekhleb
194 ընթերցումներ Նոր պատմություն

Հասկանալով Twitter API-ն, որպեսզի կարողանաք նախագծել ձեր սեփականը

կողմից Oleksii Trekhleb22m2024/12/16
Read on Terminal Reader

Չափազանց երկար; Կարդալ

Այս հոդվածում մենք ուսումնասիրում ենք, թե ինչպես է նախագծված X (Twitter) տնային ժամանակացույցը (x.com/home) API-ն և ինչ մոտեցումներ են նրանք օգտագործում բազմաթիվ մարտահրավերներ լուծելու համար:
featured image - Հասկանալով Twitter API-ն, որպեսզի կարողանաք նախագծել ձեր սեփականը
Oleksii Trekhleb HackerNoon profile picture
0-item
1-item

Երբ խոսքը վերաբերում է համակարգի API-ի նախագծմանը, ծրագրային ապահովման ինժեներները հաճախ դիտարկում են տարբեր տարբերակներ, ինչպիսիք են REST vs RPC vs GraphQL (կամ այլ հիբրիդային մոտեցումներ)՝ որոշակի առաջադրանքի կամ նախագծի համար լավագույնս համապատասխանությունը որոշելու համար:


Այս հոդվածում մենք ուսումնասիրում ենք, թե ինչպես է նախագծված X ( Twitter ) տնային ժամանակացույցը (x.com/home) API-ն և ինչ մոտեցումներ են նրանք օգտագործում հետևյալ մարտահրավերները լուծելու համար.

  • Ինչպես ստանալ թվիթների ցանկը

  • Ինչպես կատարել տեսակավորում և էջադրում

  • Ինչպես վերադարձնել հիերարխիկ/կապված սուբյեկտները (թվիթեր, օգտատերեր, լրատվամիջոցներ)

  • Ինչպես ստանալ թվիթերի մանրամասները

  • Ինչպես «լայքել» թվիթը


Մենք կուսումնասիրենք այս մարտահրավերները միայն API-ի մակարդակում՝ հետին պլանի իրականացումը դիտարկելով որպես սև արկղ, քանի որ մենք չունենք մուտք դեպի հետին պլանի կոդը:


Այստեղ ճշգրիտ հարցումներն ու պատասխանները ցույց տալը կարող է դժվար լինել և դժվար է հետևել, քանի որ խորապես տեղադրված և կրկնվող առարկաները դժվար է կարդալ: Որպեսզի ավելի հեշտ լինի տեսնել հարցում/պատասխանի օգտակար բեռնվածքի կառուցվածքը, ես փորձել եմ «մուտքագրել» հիմնական ժամանակացույցի API-ն TypeScript-ում: Այսպիսով, երբ խոսքը վերաբերում է հարցում/պատասխանի օրինակներին, ես կօգտագործեմ հարցման և պատասխանի տեսակները իրական JSON օբյեկտների փոխարեն: Նաև հիշեք, որ տեսակները պարզեցված են, և շատ հատկություններ բաց են թողնվել հակիրճ լինելու համար:


Դուք կարող եք գտնել բոլոր տեսակի տեսակներ/x.ts ֆայլ կամ այս հոդվածի ներքևում՝ «Հավելված. Բոլոր տեսակները մեկ տեղում» բաժնում:

Թվիթերների ցանկը բեռնվում է

Վերջնակետը և հարցման/պատասխանի կառուցվածքը

Տնային ժամանակացույցի համար թվիթների ցանկը բեռնելը սկսվում է POST հարցումով հետևյալ վերջնակետին.


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


Ահա պարզեցված հարցման մարմնի տեսակը.


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


Եվ ահա պարզեցված արձագանքման մարմնի տեսակը (մենք ավելի խորը կանդրադառնանք ստորև պատասխանի ենթատիպերին).


 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;


Հետաքրքիր է նշել այստեղ, որ տվյալների «ստանալը» կատարվում է «POSTing»-ի միջոցով, որը սովորական չէ REST-ի նման API-ի համար, սակայն սովորական է GraphQL-ի նման API-ի համար: Նաև URL-ի graphql մասը ցույց է տալիս, որ X-ն օգտագործում է GraphQL համը իրենց API-ի համար:


Ես այստեղ օգտագործում եմ «համ» բառը, քանի որ հարցման մարմինն ինքնին մաքուր տեսք չունի 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 # ... } # ... } }


Այստեղ ենթադրվում է, որ տնային ժամանակացույցի API-ն մաքուր GraphQL API չէ, այլ մի քանի մոտեցումների խառնուրդ է: Նման POST հարցումով պարամետրերը փոխանցելը ավելի մոտ է «ֆունկցիոնալ» RPC զանգին: Բայց միևնույն ժամանակ, թվում է, թե GraphQL-ի առանձնահատկությունները կարող են օգտագործվել ինչ-որ տեղ HomeTimeline վերջնակետի կարգավորիչի/վերահսկիչի հետևում: Նման խառնուրդը կարող է առաջանալ նաև ժառանգական ծածկագրի կամ շարունակական միգրացիայի պատճառով: Բայց նորից եմ ասում, սրանք ընդամենը իմ ենթադրություններն են։


Դուք կարող եք նաև նկատել, որ նույն TimelineRequest.queryId օգտագործվում է API URL-ում, ինչպես նաև API հարցումների մարմնում: Այս queryId-ը, ամենայն հավանականությամբ, ստեղծվում է հետնամասում, այնուհետև այն տեղադրվում է main.js փաթեթում, այնուհետև այն օգտագործվում է հետնամասից տվյալները բեռնելիս: Ինձ համար դժվար է հասկանալ, թե ինչպես է օգտագործվում այս queryId , քանի որ X-ի հետնամասը մեր դեպքում սև արկղ է: Բայց, կրկին, այստեղ ենթադրությունը կարող է լինել այն, որ դա կարող է անհրաժեշտ լինել կատարողականի որոշակի օպտիմիզացման համար (վերօգտագործելով որոշ նախապես հաշվարկված հարցումների արդյունքներ), քեշավորման (Apollo-ին առնչվող?), վրիպազերծման համար (միանալ տեղեկամատյաններին queryId-ով): կամ հետագծման/հետագծման նպատակներով:

Հետաքրքիր է նաև նշել, որ TimelineResponse ը պարունակում է ոչ թե թվիթների ցանկ, այլ հրահանգների ցանկ, օրինակ՝ «ավելացնել թվիթը ժամանակացույցին» (տես TimelineAddEntries տեսակը), կամ «դադարեցնել ժամանակացույցը» (տես TimelineTerminateTimeline տեսակ):


TimelineAddEntries հրահանգն ինքնին կարող է նաև պարունակել տարբեր տեսակի սուբյեկտներ.

  • Թվիթեր — տես TimelineItem տեսակը
  • Կուրսորներ — տես TimelineCursor տեսակը
  • Խոսակցություններ/մեկնաբանություններ/թելեր — տես TimelineModule տեսակը


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


Սա հետաքրքիր է երկարաձգելիության տեսանկյունից, քանի որ այն թույլ է տալիս ավելի լայն տեսականի, թե ինչ կարելի է մատուցել տնային ժամանակացույցում՝ առանց API-ի չափից շատ շտկելու:

Էջավորում

TimelineRequest.variables.count հատկությունը սահմանում է, թե քանի թվիթ ենք ուզում ստանալ միանգամից (մեկ էջի համար): Կանխադրվածը 20 է: Այնուամենայնիվ, TimelineAddEntries.entries զանգվածում կարող են վերադարձվել ավելի քան 20 թվիթներ: Օրինակ, զանգվածը կարող է պարունակել 37 գրառում առաջին էջի բեռնման համար, քանի որ այն ներառում է թվիթներ (29), ամրացված թվիթներ (1), խթանված թվիթներ (5) և էջադրման կուրսորներ (2): Ես վստահ չեմ, թե ինչու կան 29 կանոնավոր թվիթներ, սակայն պահանջված թվով 20:


TimelineRequest.variables.cursor ը պատասխանատու է կուրսորի վրա հիմնված էջավորման համար:


« Կուրսորային էջավորումն առավել հաճախ օգտագործվում է իրական ժամանակի տվյալների համար՝ նոր գրառումների ավելացման հաճախականության պատճառով, և որովհետև տվյալներ կարդալիս դուք հաճախ առաջինը տեսնում եք վերջին արդյունքները: Այն վերացնում է տարրերը բաց թողնելու և նույն նյութը մեկից ավելի անգամ ցուցադրելու հնարավորությունը: կուրսորի վրա հիմնված էջադրում, հաստատուն ցուցիչ (կամ կուրսոր) օգտագործվում է հետևելու համար, թե տվյալների հավաքածուից որտեղից պետք է բերվեն հաջորդ տարրերը»: Տեսեք Օֆսեթ էջադրում ընդդեմ կուրսորային էջավորման թեմա համատեքստի համար:


Թվիթերների ցանկն առաջին անգամ բեռնելիս TimelineRequest.variables.cursor ը դատարկ է, քանի որ մենք ցանկանում ենք բեռնել լավագույն թվիթները անձնավորված թվիթների լռելյայն (ամենայն հավանականությամբ, նախապես հաշվարկված) ցուցակից:


Այնուամենայնիվ, պատասխանում, թվիթերի տվյալների հետ մեկտեղ, backend-ը վերադարձնում է նաև կուրսորի գրառումները: Ահա պատասխանի տիպի հիերարխիան՝ 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'; }; };


Յուրաքանչյուր էջ պարունակում է թվիթների ցանկ՝ «վերևի» և «ներքևի» կուրսորների հետ միասին.


Էջի տվյալները բեռնվելուց հետո մենք կարող ենք ընթացիկ էջից գնալ երկու ուղղություններով և ստանալ կա՛մ «նախորդ/հին» թվիթները՝ օգտագործելով «ներքևի» կուրսորը, կա՛մ «հաջորդ/նոր» թվիթները՝ օգտագործելով «վերևի» կուրսորը: Իմ ենթադրությունն այն է, որ «հաջորդ» թվիթները «վերևի» կուրսորով բեռնելը տեղի է ունենում երկու դեպքում. քեշավորված գրառումներ չկան կամ եթե նախորդ գրառումները ջնջվել են կատարողականի պատճառով):


X-ի կուրսորն ինքնին կարող է այսպիսի տեսք ունենալ՝ DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA : API-ի որոշ ձևավորումներում կուրսորը կարող է լինել Base64 կոդավորված տող, որը պարունակում է ցանկի վերջին մուտքի ID-ն կամ վերջին տեսած մուտքի ժամանակի դրոշմը: Օրինակ՝ eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} , այնուհետև այս տվյալները օգտագործվում են տվյալների բազան համապատասխանաբար հարցումներ անելու համար: X API-ի դեպքում, թվում է, թե կուրսորը Base64-ը վերծանվում է որոշ հատուկ երկուական հաջորդականության մեջ, որը կարող է պահանջել լրացուցիչ ապակոդավորում՝ դրանից որևէ իմաստ ստանալու համար (այսինքն՝ Protobuf հաղորդագրության սահմանումների միջոցով): Քանի որ մենք չգիտենք, թե արդյոք դա .proto կոդավորում է, ինչպես նաև մենք չգիտենք .proto հաղորդագրության սահմանումը, կարող ենք պարզապես ենթադրել, որ հետնամասը գիտի, թե ինչպես պետք է հարցումներ կատարել թվիթների հաջորդ խմբաքանակի վրա՝ հիմնվելով կուրսորի տողի վրա:


TimelineResponse.variables.seenTweetIds պարամետրը օգտագործվում է սերվերին տեղեկացնելու համար, թե հաճախորդն արդեն տեսել է, թե որ թվիթներն է անսահման պտտվող ընթացիկ էջից: Սա, ամենայն հավանականությամբ, օգնում է ապահովել, որ սերվերը չի ներառում կրկնօրինակ թվիթներ արդյունքների հաջորդ էջերում:

Կապակցված/հիերարխիկ սուբյեկտներ

API-ներում, ինչպիսին է հիմնական ժամանակացույցը (կամ Գլխավոր էջը) լուծելու մարտահրավերներից մեկը՝ պարզելն է, թե ինչպես վերադարձնել կապակցված կամ հիերարխիկ սուբյեկտները (այսինքն՝ tweet → user , tweet → media , media → author և այլն).


  • Արդյո՞ք մենք պետք է միայն սկզբում վերադարձնենք թվիթների ցանկը, այնուհետև առբերենք կախյալ սուբյեկտները (ինչպես օգտատերերի տվյալները) առանձին հարցումների մի փունջ՝ ըստ պահանջի:
  • Թե՞ մենք պետք է միանգամից վերադարձնենք բոլոր տվյալները՝ ավելացնելով առաջին բեռնման ժամանակն ու չափը, բայց խնայելով ժամանակը բոլոր հետագա զանգերի համար:
    • Արդյո՞ք մենք պետք է նորմալացնենք տվյալները այս դեպքում՝ բեռնաթափման չափը նվազեցնելու համար (այսինքն, երբ նույն օգտատերը բազմաթիվ թվիթների հեղինակ է և մենք ցանկանում ենք խուսափել օգտվողի տվյալները անընդհատ կրկնելուց յուրաքանչյուր թվիթում):
  • Թե՞ դա պետք է լինի վերը նշված մոտեցումների համակցում։


Տեսնենք, թե ինչպես է X-ը դա անում:

Ավելի վաղ 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[]; };


Հետաքրքիրն այստեղ այն է, որ կախված տվյալների մեծ մասը, ինչպիսիք են tweet → media և tweet → author ներառված են առաջին զանգի պատասխանի մեջ (հետագա հարցումներ չկան):


Բացի այդ, User և Media կապերը Tweet սուբյեկտների հետ նորմալացված չեն (եթե երկու թվիթներ ունեն նույն հեղինակը, դրանց տվյալները կկրկնվեն թվիթերի յուրաքանչյուր օբյեկտում): Բայց թվում է, որ դա պետք է լավ լինի, քանի որ կոնկրետ օգտատիրոջ համար տնային ժամանակացույցի շրջանակներում թվիթերը կհեղինակվեն բազմաթիվ հեղինակների կողմից, և կրկնությունները հնարավոր են, բայց հազվադեպ:


Իմ ենթադրությունն այն էր, որ UserTweets API-ն (որը մենք այստեղ չենք լուսաբանում), որը պատասխանատու է մեկ կոնկրետ օգտատիրոջ թվիթերը բեռնելու համար, այլ կերպ կվերաբերվի դրան, բայց, ըստ երևույթին, դա այդպես չէ: UserTweets ը վերադարձնում է միևնույն օգտատիրոջ թվիթերի ցանկը և յուրաքանչյուր թվիթի համար կրկին ու կրկին տեղադրում է նույն օգտվողի տվյալները: Հետաքրքիր է։ Միգուցե մոտեցման պարզությունը գերազանցում է որոշ տվյալների չափը (գուցե օգտագործողի տվյալները համարվում են բավականին փոքր չափերով): Ես վստահ չեմ:


Մեկ այլ դիտարկում սուբյեկտների փոխհարաբերությունների վերաբերյալ այն է, որ Media կազմակերպությունը նաև հղում ունի User (հեղինակին): Բայց դա անում է ոչ թե ուղղակի բովանդակության ներդրման միջոցով, ինչպես անում է Tweet ը, այլ ավելի շուտ այն կապում է Media.source_user_id_str հատկության միջոցով:


«Մեկնաբանությունները» (որոնք իրենց բնույթով նաև «թվիթեր» են) տնային ժամանակացույցի յուրաքանչյուր «թվիթի» համար բացարձակապես չեն վերցվում: Թվիթերի շարանը տեսնելու համար օգտատերը պետք է սեղմի թվիթը՝ դրա մանրամասն տեսքը տեսնելու համար: Թվիթերի շարանը կբերվի՝ զանգահարելով TweetDetail վերջնակետը (այդ մասին ավելին «Tweet մանրամասն էջում» բաժնում):


Մեկ այլ սուբյեկտ, որն ունի յուրաքանչյուր Tweet , FeedbackActions ն է (այսինքն՝ «Առաջարկել ավելի քիչ հաճախ» կամ «Տեսնել ավելի քիչ»): Պատասխանող օբյեկտում FeedbackActions ի պահպանման եղանակը տարբերվում է User և Media օբյեկտների պահպանման եղանակից: Մինչ User և Media սուբյեկտները Tweet մաս են կազմում, FeedbackActions ը պահվում է առանձին TimelineItem.content.feedbackInfo.feedbackKeys զանգվածում և կապված են ActionKey միջոցով: Դա մի փոքր անակնկալ էր ինձ համար, քանի որ կարծես թե այնպես չէ, որ որևէ գործողություն կարող է կրկնակի օգտագործել: Կարծես թե մեկ գործողություն օգտագործվում է միայն մեկ կոնկրետ թվիթերի համար: Այսպիսով, թվում է, թե FeedbackActions ը կարող է ներառվել յուրաքանչյուր թվիթում այնպես, ինչպես Media կազմակերպությունները: Բայց այստեղ կարող եմ բացակայել որոշ թաքնված բարդություն (օրինակ, այն, որ յուրաքանչյուր գործողություն կարող է ունենալ երեխաների գործողություններ):

Գործողությունների մասին ավելի շատ մանրամասներ՝ ստորև ներկայացված «Թվիթերի գործողություններ» բաժնում:

Տեսակավորում

Ժամանակացույցի գրառումների տեսակավորման կարգը սահմանվում է հետնամասի կողմից՝ 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', }; };


sortIndex ն ինքնին կարող է նման լինել '1867231621095096312' : Այն, հավանաբար, ուղղակիորեն համապատասխանում է կամ բխում է a-ից Ձյան փաթիլ ID .


Փաստորեն, ID-ների մեծ մասը, որոնք տեսնում եք պատասխանում (tweet ID-ներ) հետևում են «Snowflake ID» կոնվենցիային և նման են '1867231621095096312' :


Եթե սա օգտագործվում է թվիթների նման միավորները տեսակավորելու համար, ապա համակարգը օգտագործում է Snowflake ID-ների բնորոշ ժամանակագրական տեսակավորումը: Ավելի բարձր տեսակավորման ինդեքս արժեք ունեցող թվիթները կամ առարկաները (ավելի վերջերս ժամանակի դրոշմ) ավելի բարձր են հայտնվում լրահոսում, մինչդեռ ավելի ցածր արժեքներ ունեցողները (ավելի հին ժամանակի դրոշմ) ավելի ցածր են հայտնվում լրահոսում:


Ահա Snowflake ID-ի քայլ առ քայլ վերծանումը (մեր դեպքում՝ sortIndex ) 1867231621095096312 :

  • Քաղեք ժամանակի դրոշմակնիքը .
    • Ժամանակի դրոշմը ստացվում է Snowflake ID-ն 22 բիթով աջ տեղափոխելով (տվյալների կենտրոնի, աշխատողի ID-ի և հաջորդականության համար ստորին 22 բիթերը հեռացնելու համար). 1867231621095096312 → 445182709954
  • Ավելացնել Twitter-ի դարաշրջանը .
    • Twitter-ի հարմարեցված դարաշրջանը (1288834974657) ավելացնելով այս ժամանակի դրոշմանիշին, UNIX-ի ժամանակային դրոշմը միլիվայրկյաններով՝ 445182709954 + 1288834974657 → 1734017684611ms
  • Փոխարկել մարդու համար ընթեռնելի ամսաթվի .
    • UNIX-ի ժամադրոշմը UTC ամսաթվայինի վերածելը տալիս է՝ 1734017684611ms → 2024-12-12 15:34:44.611 (UTC)


Այսպիսով, այստեղ կարող ենք ենթադրել, որ հիմնական ժամանակացույցի թվիթերը դասավորված են ժամանակագրական կարգով:

Թվիթերի գործողություններ

Յուրաքանչյուր թվիթ ունի «Գործողություններ» մենյու:


Յուրաքանչյուր թվիթի գործողությունները գալիս են հետնամասից՝ 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' }; };


Հետաքրքիր է այստեղ, որ գործողությունների այս հարթ զանգվածն իրականում ծառ է (թե՞ գրաֆիկ, ես չեմ ստուգել), քանի որ յուրաքանչյուր գործողություն կարող է ունենալ զավակային գործողություններ (տես TimelineAction.value.childKeys զանգվածը): Սա իմաստ ունի, օրինակ, երբ օգտատերը սեղմելուց հետո «Չեմ հավանում» գործողության վրա, հաջորդ քայլը կարող է լինել ցույց տալ «Այս գրառումը տեղին չէ» գործողությունը՝ որպես բացատրելու, թե ինչու է օգտատերը դուր չի գալիս թվիթը:

Թվիթերի մանրամասների էջ

Երբ օգտատերը ցանկանում է տեսնել թվիթերի մանրամասն էջը (այսինքն՝ տեսնելու մեկնաբանությունների/թվիթերի շարանը), օգտատերը սեղմում է թվիթը և 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}


Ինձ այստեղ հետաքրքիր էր, թե ինչու է թվիթների ցուցակը բեռնվում POST զանգի միջոցով, բայց թվիթերի յուրաքանչյուր մանրամասն բեռնվում է GET զանգի միջոցով: Թվում է, թե անհամապատասխան է: Հատկապես նկատի ունենալով, որ հարցման նմանատիպ պարամետրերը, ինչպիսիք են query-id , features և այլն, այս անգամ փոխանցվում են URL-ում և ոչ թե հարցման մարմնում: Պատասխանների ձևաչափը նույնպես նման է և նորից օգտագործում է ցուցակի զանգի տեսակները: Ես վստահ չեմ, թե ինչու է այդպես: Բայց ևս մեկ անգամ, ես վստահ եմ, որ կարող եմ այստեղ բացակայել որոշակի ֆոնային բարդություն:

Ահա պարզեցված արձագանքման մարմնի տեսակները.


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


Պատասխանը բավականին նման է (իր տեսակներով) ցուցակի պատասխանին, այնպես որ մենք այստեղ շատ երկար չենք անի:


Հետաքրքիր նրբերանգներից մեկն այն է, որ յուրաքանչյուր թվիթի «մեկնաբանությունները» (կամ խոսակցությունները) իրականում այլ թվիթներ են (տես TimelineModule տեսակը): Այսպիսով, թվիթերի շարանը շատ նման է տնային ժամանակացույցի հոսքին՝ ցուցադրելով TimelineTweet գրառումների ցանկը: Սա նրբագեղ տեսք ունի: API-ի դիզայնի համընդհանուր և վերօգտագործելի մոտեցման լավ օրինակ:

Հավանում եմ թվիթը

Երբ օգտատերը հավանում է թվիթը, կատարվում է POST հարցումը հետևյալ վերջնակետին.


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


Ահա խնդրանքի մարմնի տեսակները.


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


Ահա արձագանքման մարմնի տեսակները.


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


Պարզ տեսք ունի և նաև նման է API-ի ձևավորման RPC-ի նման մոտեցմանը:


Եզրակացություն

Մենք անդրադարձել ենք տնային ժամանակացույցի API-ի նախագծման որոշ հիմնական մասերին՝ դիտելով X-ի API-ի օրինակը: Ճանապարհին ես որոշ ենթադրություններ արեցի իմ գիտելիքների չափով: Ես հավատում եմ, որ որոշ բաներ կարող էի սխալ մեկնաբանել և որոշ բարդ նրբերանգներ բաց թողած լինեի: Բայց նույնիսկ դա նկատի ունենալով, հուսով եմ, որ դուք որոշ օգտակար պատկերացումներ եք ստացել այս բարձր մակարդակի ակնարկից, մի բան, որը կարող եք կիրառել ձեր հաջորդ API դիզայնի նիստում:


Սկզբում ես պլան ունեի անցնել նմանատիպ թոփ-տեխնոլոգիական կայքերով՝ Facebook-ից, Reddit-ից, YouTube-ից և մյուսներից որոշ պատկերացումներ ստանալու և մարտերում փորձարկված լավագույն փորձն ու լուծումները հավաքելու համար: Վստահ չեմ, որ ժամանակ կգտնեմ դա անելու համար: Կտեսնեմ. Բայց դա կարող է լինել հետաքրքիր վարժություն:

Հավելված. Բոլոր տեսակները մեկ վայրում

Հղման համար ես այստեղ ավելացնում եմ բոլոր տեսակները մեկ քայլով: Դուք կարող եք նաև գտնել բոլոր տեսակները տեսակներ/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;