paint-brush
Разуменне Twitter API, каб вы маглі стварыць свой уласныпа@trekhleb
194 чытанні Новая гісторыя

Разуменне Twitter API, каб вы маглі стварыць свой уласны

па Oleksii Trekhleb22m2024/12/16
Read on Terminal Reader

Занадта доўга; Чытаць

У гэтым артыкуле мы даследуем, як створаны API хатняй часовай шкалы X (Twitter) (x.com/home) і якія падыходы яны выкарыстоўваюць для вырашэння шматлікіх задач.
featured image - Разуменне Twitter API, каб вы маглі стварыць свой уласны
Oleksii Trekhleb HackerNoon profile picture
0-item
1-item

Калі справа даходзіць да распрацоўкі API сістэмы, інжынеры-праграмісты часта разглядаюць розныя варыянты, напрыклад REST супраць RPC супраць GraphQL (або іншыя гібрыдныя падыходы), каб вызначыць найбольш прыдатны для канкрэтнай задачы або праекта.


У гэтым артыкуле мы даследуем, як распрацаваны API хатняй часовай шкалы X ( Twitter ) (x.com/home) і якія падыходы яны выкарыстоўваюць для вырашэння наступных задач:

  • Як атрымаць спіс твітаў

  • Як зрабіць сартаванне і пагінацыю

  • Як вярнуць іерархічныя/звязаныя аб'екты (твіты, карыстальнікі, медыя)

  • Як атрымаць падрабязную інфармацыю пра твіт

  • Як "лайкаць" твіт


Мы будзем даследаваць гэтыя праблемы толькі на ўзроўні 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.


Я выкарыстоўваю тут слова "водар", таму што само цела запыту не выглядае чыстым Запыт 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 хатняй часовай шкалы не з'яўляецца чыстым API GraphQL, а з'яўляецца спалучэннем некалькіх падыходаў . Падобная перадача параметраў у запыце POST выглядае бліжэй да "функцыянальнага" выкліку RPC. Але ў той жа час здаецца, што функцыі GraphQL могуць выкарыстоўвацца дзесьці на бэкендзе за апрацоўшчыкам/кантролерам канчатковай кропкі HomeTimeline . Такая сумесь таксама можа быць выклікана састарэлым кодам або нейкай бягучай міграцыяй. Але зноў жа, гэта толькі мае здагадкі.


Вы таксама можаце заўважыць, што той жа TimelineRequest.queryId выкарыстоўваецца ў URL API, а таксама ў целе запыту 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 пусты, бо мы хочам атрымаць лепшыя твіты з стандартнага (хутчэй за ўсё, загадзя вылічанага) спісу персаналізаваных твітаў.


Аднак у адказе, разам з дадзенымі твіта, бэкэнд таксама вяртае запісы курсора. Вось іерархія тыпаў адказаў: 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, які змяшчае ідэнтыфікатар апошняга запісу ў спісе або пазнаку часу апошняга запісу, які вы бачылі. Напрыклад: 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 не нармалізуюцца (калі два твіты маюць аднаго аўтара, іх даныя будуць паўтарацца ў кожным аб'екце твіта). Але здаецца, што гэта павінна быць нармальна, бо ў межах хатняй шкалы часу для канкрэтнага карыстальніка твіты будуць напісаны многімі аўтарамі, і паўторы магчымыя, але рэдкія.


Я меркаваў, што API UserTweets (яго мы тут не разглядаем), які адказвае за атрыманне твітаў аднаго канкрэтнага карыстальніка, будзе апрацоўваць гэта па-рознаму, але, відаць, гэта не так. UserTweets вяртае спіс твітаў аднаго карыстальніка і ўбудоўвае адны і тыя ж дадзеныя карыстальніка зноў і зноў для кожнага твіта. Гэта цікава. Магчыма, прастата падыходу перавышае накладныя выдаткі на памер дадзеных (магчыма, карыстальніцкія дадзеныя лічацца даволі малымі па памеры). Я не ўпэўнены.


Яшчэ адно назіранне адносна адносін суб'ектаў заключаецца ў тым, што суб'ект Media таксама мае спасылку на User (аўтара). Але ён робіць гэта не праз прамое ўбудаванне аб'ектаў, як гэта робіць аб'ект Tweet , а спасылаецца праз уласцівасць Media.source_user_id_str .


«Каментарыі» (якія таксама з'яўляюцца «твітамі» па сваёй прыродзе) да кожнага «твіту» на хатняй часовай шкале наогул не выбіраюцца. Каб убачыць ланцужок твітаў, карыстальнік павінен націснуць на твіт, каб убачыць яго падрабязны выгляд. Паток твітаў будзе атрыманы шляхам выкліку канчатковай кропкі TweetDetail (падрабязней пра гэта ў раздзеле «Старонка з падрабязнасцямі твітаў» ніжэй).


Яшчэ адна сутнасць, якую мае кожны Tweet , - гэта FeedbackActions (напрыклад, «Рэкамендаваць радзей» або «Глядзіць менш»). Спосаб захавання FeedbackActions у аб'екце адказу адрозніваецца ад спосабу захоўвання аб'ектаў User і Media . Хоць аб'екты User і Media з'яўляюцца часткай Tweet , FeedbackActions захоўваюцца асобна ў масіве TimelineItem.content.feedbackInfo.feedbackKeys і звязаны праз ActionKey . Гэта было невялікім сюрпрызам для мяне, таму што гэта не так, што любое дзеянне можна выкарыстоўваць паўторна. Падобна на тое, што адно дзеянне выкарыстоўваецца толькі для аднаго канкрэтнага твіта. Такім чынам, выглядае, што FeedbackActions можна ўбудаваць у кожны твіт гэтак жа, як і Media . Але я магу прапусціць некаторыя схаваныя складанасці (напрыклад, той факт, што кожнае дзеянне можа мець даччыныя дзеянні).

Больш падрабязная інфармацыя аб дзеяннях у раздзеле «Дзеянні ў Twitter» ніжэй.

Сартаванне

Парадак сартавання запісаў часовай шкалы вызначаецца бэкэндам праз уласцівасці 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 сняжынкі .


Фактычна большасць ідэнтыфікатараў, якія вы бачыце ў адказе (ідэнтыфікатары твітаў), прытрымліваюцца канвенцыі «ідэнтыфікатар сняжынкі» і выглядаюць як '1867231621095096312' .


Калі гэта выкарыстоўваецца для сартавання такіх аб'ектаў, як твіты, сістэма выкарыстоўвае ўласцівую храналагічную сартаванне ідэнтыфікатараў Snowflake. Твіты або аб'екты з больш высокім значэннем sortIndex (больш свежая пазнака часу) з'яўляюцца вышэй у стужцы, а аб'екты з меншымі значэннямі (старэйшая пазнака часу) - ніжэй.


Вось пакрокавая расшыфроўка ідэнтыфікатара Сняжынкі (у нашым выпадку sortIndex ) 1867231621095096312 :

  • Выняць пазнаку часу :
    • Пазнака часу атрымліваецца шляхам зрушэння ідэнтыфікатара сняжынкі ўправа на 22 біта (каб выдаліць малодшыя 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', } }


Выглядае проста, а таксама нагадвае RPC-падобны падыход да дызайну API.


Заключэнне

Мы закранулі некаторыя асноўныя часткі дызайну API хатняй часовай шкалы, разглядаючы прыклад API X. Я зрабіў некаторыя здагадкі на гэтым шляху, наколькі мне было вядома. Я лічу, што некаторыя рэчы я мог няправільна вытлумачыць і прапусціць некаторыя складаныя нюансы. Але нават маючы гэта на ўвазе, я спадзяюся, што вы атрымалі карысную інфармацыю з гэтага высокаўзроўневага агляду, тое, што вы маглі б прымяніць на сваім наступным занятку па распрацоўцы 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;