Երբ խոսքը վերաբերում է համակարգի API-ի նախագծմանը, ծրագրային ապահովման ինժեներները հաճախ դիտարկում են տարբեր տարբերակներ, ինչպիսիք են
Այս հոդվածում մենք ուսումնասիրում ենք, թե ինչպես է նախագծված X ( Twitter ) տնային ժամանակացույցը (x.com/home) API-ն և ինչ մոտեցումներ են նրանք օգտագործում հետևյալ մարտահրավերները լուծելու համար.
Ինչպես ստանալ թվիթների ցանկը
Ինչպես կատարել տեսակավորում և էջադրում
Ինչպես վերադարձնել հիերարխիկ/կապված սուբյեկտները (թվիթեր, օգտատերեր, լրատվամիջոցներ)
Ինչպես ստանալ թվիթերի մանրամասները
Ինչպես «լայքել» թվիթը
Մենք կուսումնասիրենք այս մարտահրավերները միայն API-ի մակարդակում՝ հետին պլանի իրականացումը դիտարկելով որպես սև արկղ, քանի որ մենք չունենք մուտք դեպի հետին պլանի կոդը:
Այստեղ ճշգրիտ հարցումներն ու պատասխանները ցույց տալը կարող է դժվար լինել և դժվար է հետևել, քանի որ խորապես տեղադրված և կրկնվող առարկաները դժվար է կարդալ: Որպեսզի ավելի հեշտ լինի տեսնել հարցում/պատասխանի օգտակար բեռնվածքի կառուցվածքը, ես փորձել եմ «մուտքագրել» հիմնական ժամանակացույցի API-ն TypeScript-ում: Այսպիսով, երբ խոսքը վերաբերում է հարցում/պատասխանի օրինակներին, ես կօգտագործեմ հարցման և պատասխանի տեսակները իրական JSON օբյեկտների փոխարեն: Նաև հիշեք, որ տեսակները պարզեցված են, և շատ հատկություններ բաց են թողնվել հակիրճ լինելու համար:
Դուք կարող եք գտնել բոլոր տեսակի
Տնային ժամանակացույցի համար թվիթների ցանկը բեռնելը սկսվում է 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-ի համար:
Ես այստեղ օգտագործում եմ «համ» բառը, քանի որ հարցման մարմինն ինքնին մաքուր տեսք չունի
# 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-ների մեծ մասը, որոնք տեսնում եք պատասխանում (tweet ID-ներ) հետևում են «Snowflake ID» կոնվենցիային և նման են '1867231621095096312'
:
Եթե սա օգտագործվում է թվիթների նման միավորները տեսակավորելու համար, ապա համակարգը օգտագործում է Snowflake ID-ների բնորոշ ժամանակագրական տեսակավորումը: Ավելի բարձր տեսակավորման ինդեքս արժեք ունեցող թվիթները կամ առարկաները (ավելի վերջերս ժամանակի դրոշմ) ավելի բարձր են հայտնվում լրահոսում, մինչդեռ ավելի ցածր արժեքներ ունեցողները (ավելի հին ժամանակի դրոշմ) ավելի ցածր են հայտնվում լրահոսում:
Ահա Snowflake ID-ի քայլ առ քայլ վերծանումը (մեր դեպքում՝ sortIndex
) 1867231621095096312
:
1867231621095096312 → 445182709954
445182709954 + 1288834974657 → 1734017684611ms
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-ից և մյուսներից որոշ պատկերացումներ ստանալու և մարտերում փորձարկված լավագույն փորձն ու լուծումները հավաքելու համար: Վստահ չեմ, որ ժամանակ կգտնեմ դա անելու համար: Կտեսնեմ. Բայց դա կարող է լինել հետաքրքիր վարժություն:
Հղման համար ես այստեղ ավելացնում եմ բոլոր տեսակները մեկ քայլով: Դուք կարող եք նաև գտնել բոլոր տեսակները
/** * 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;