Ketika merancang API sistem, para insinyur perangkat lunak sering mempertimbangkan berbagai opsi seperti (atau pendekatan hibrida lainnya) untuk menentukan pendekatan yang paling cocok untuk tugas atau proyek tertentu. REST vs RPC vs GraphQL Dalam artikel ini, kami membahas bagaimana API linimasa beranda ( ) (x.com/home) dirancang dan pendekatan apa yang mereka gunakan untuk mengatasi tantangan berikut: X Twitter Cara mengambil daftar tweet Cara melakukan penyortiran dan pagination Cara mengembalikan entitas hierarkis/tertaut (tweet, pengguna, media) Cara mendapatkan detail tweet Cara "menyukai" tweet Kami hanya akan mengeksplorasi tantangan ini pada level API, memperlakukan implementasi backend sebagai kotak hitam, karena kami tidak memiliki akses ke kode backend itu sendiri. Menampilkan permintaan dan respons yang tepat di sini mungkin merepotkan dan sulit diikuti karena objek yang sangat bersarang dan berulang sulit dibaca. Untuk mempermudah melihat struktur muatan permintaan/respons, saya telah mencoba untuk "mengetik" API linimasa beranda dalam TypeScript. Jadi, jika menyangkut contoh permintaan/respons, saya akan menggunakan permintaan dan respons, bukan objek JSON yang sebenarnya. Selain itu, ingatlah bahwa tipe-tipe tersebut disederhanakan dan banyak properti dihilangkan demi singkatnya. tipe Anda dapat menemukan semua jenis di jenis/x.ts file atau di bagian bawah artikel ini di bagian "Lampiran: Semua jenis di satu tempat". Mengambil daftar tweet Titik akhir dan struktur permintaan/respons Pengambilan daftar tweet untuk linimasa beranda dimulai dengan permintaan ke titik akhir berikut: POST POST https://x.com/i/api/graphql/{query-id}/HomeTimeline Berikut ini adalah tipe badan yang disederhanakan: permintaan 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; // ... } Dan berikut ini adalah tipe isi yang disederhanakan (kita akan membahas lebih dalam subtipe respons di bawah): respons 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; Menarik untuk dicatat di sini, bahwa "mendapatkan" data dilakukan melalui "POSTing", yang tidak umum untuk API seperti REST tetapi umum untuk API seperti GraphQL. Selain itu, bagian dari URL menunjukkan bahwa X menggunakan GraphQL untuk API mereka. graphql Saya menggunakan kata di sini karena isi permintaan itu sendiri tidak terlihat seperti rasa murni. , di mana kita dapat menjelaskan struktur respons yang diperlukan, dengan mencantumkan semua properti yang ingin kita ambil: "rasa" Kueri 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 # ... } # ... } } Asumsinya di sini adalah bahwa API linimasa beranda bukanlah API GraphQL murni, tetapi merupakan . Melewati parameter dalam permintaan POST seperti ini tampaknya lebih dekat dengan panggilan RPC "fungsional". Namun pada saat yang sama, tampaknya fitur GraphQL mungkin digunakan di suatu tempat di bagian belakang di belakang pengendali/pengendali titik akhir . Campuran seperti ini mungkin juga disebabkan oleh kode lama atau semacam migrasi yang sedang berlangsung. Namun sekali lagi, ini hanyalah spekulasi saya. campuran dari beberapa pendekatan HomeTimeline Anda mungkin juga memperhatikan bahwa yang sama digunakan di URL API serta di badan permintaan API. QueryId ini kemungkinan besar dibuat di backend, kemudian disematkan di bundel , lalu digunakan saat mengambil data dari backend. Sulit bagi saya untuk memahami bagaimana ini digunakan secara tepat karena backend X adalah kotak hitam dalam kasus kami. Namun, sekali lagi, spekulasi di sini mungkin adalah, queryId ini mungkin diperlukan untuk beberapa jenis pengoptimalan kinerja (menggunakan kembali beberapa hasil kueri yang telah dihitung sebelumnya?), caching (terkait Apollo?), debugging (menggabungkan log dengan queryId?), atau tujuan pelacakan/penelusuran. TimelineRequest.queryId main.js queryId Menarik untuk dicatat, bahwa tidak berisi daftar tweet, melainkan daftar , seperti (lihat tipe ), atau (lihat tipe ). TimelineResponse instruksi "tambahkan tweet ke timeline" TimelineAddEntries "hentikan timeline" TimelineTerminateTimeline Instruksi sendiri juga dapat berisi berbagai jenis entitas: TimelineAddEntries Tweet — lihat tipe TimelineItem Kursor — lihat TimelineCursor Percakapan/komentar/utas — lihat tipe TimelineModule type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here }; Hal ini menarik dari sudut pandang perluasan karena memungkinkan variasi yang lebih luas dari apa yang dapat ditampilkan di timeline beranda tanpa terlalu banyak mengubah API. Paginasi Properti menetapkan berapa banyak tweet yang ingin kita ambil sekaligus (per halaman). Nilai default adalah 20. Namun, lebih dari 20 tweet dapat dikembalikan dalam array . Misalnya, array tersebut mungkin berisi 37 entri untuk pemuatan halaman pertama, karena array tersebut mencakup tweet (29), tweet yang disematkan (1), tweet yang dipromosikan (5), dan kursor pagination (2). Saya tidak yakin mengapa ada 29 tweet biasa dengan jumlah yang diminta sebesar 20. TimelineRequest.variables.count TimelineAddEntries.entries bertanggung jawab atas pagination berbasis kursor. TimelineRequest.variables.cursor " paling sering digunakan untuk data waktu nyata karena frekuensi penambahan catatan baru dan karena saat membaca data, Anda sering kali melihat hasil terbaru terlebih dahulu. Ini menghilangkan kemungkinan melewatkan item dan menampilkan item yang sama lebih dari sekali. Dalam pagination berbasis kursor, pointer konstan (atau kursor) digunakan untuk melacak di mana dalam set data item berikutnya harus diambil." Lihat Paging kursor Paginasi offset vs Paginasi kursor utas untuk konteksnya. Saat mengambil daftar tweet untuk pertama kalinya, kosong, karena kami ingin mengambil tweet teratas dari daftar tweet personalisasi default (kemungkinan besar telah dihitung sebelumnya). TimelineRequest.variables.cursor Namun, dalam respons, bersama dengan data tweet, backend juga mengembalikan entri kursor. Berikut hierarki jenis respons: : 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'; }; }; Setiap halaman berisi daftar tweet beserta kursor "atas" dan "bawah": Setelah data halaman dimuat, kita dapat berpindah dari halaman saat ini ke kedua arah dan mengambil tweet "sebelumnya/lama" menggunakan kursor "bawah" atau tweet "berikutnya/baru" menggunakan kursor "atas". Asumsi saya adalah bahwa mengambil tweet "berikutnya" menggunakan kursor "atas" terjadi dalam dua kasus: ketika tweet baru ditambahkan saat pengguna masih membaca halaman saat ini, atau ketika pengguna mulai menggulir umpan ke atas (dan tidak ada entri yang di-cache atau jika entri sebelumnya dihapus karena alasan kinerja). Kursor X itu sendiri mungkin terlihat seperti ini: . Dalam beberapa desain API, kursor mungkin berupa string yang dikodekan Base64 yang berisi id entri terakhir dalam daftar, atau stempel waktu entri terakhir yang terlihat. Misalnya: , dan kemudian, data ini digunakan untuk mengkueri basis data sebagaimana mestinya. Dalam kasus API X, sepertinya kursor sedang didekodekan Base64 menjadi beberapa urutan biner khusus yang mungkin memerlukan beberapa dekode lebih lanjut untuk mendapatkan makna darinya (yaitu melalui definisi pesan Protobuf). Karena kita tidak tahu apakah itu adalah pengodean dan juga tidak tahu definisi pesan , kita mungkin berasumsi bahwa backend tahu cara menanyakan kumpulan tweet berikutnya berdasarkan string kursor. DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} .proto .proto Parameter digunakan untuk memberi tahu server tentang tweet mana dari halaman yang sedang aktif dengan pengguliran tak terbatas yang telah dilihat klien. Hal ini kemungkinan besar membantu memastikan bahwa server tidak menyertakan tweet duplikat di halaman hasil berikutnya. TimelineResponse.variables.seenTweetIds Entitas yang terhubung/berhierarkis Salah satu tantangan yang harus dipecahkan dalam API seperti timeline beranda (atau Home Feed) adalah mencari cara untuk mengembalikan entitas yang terhubung atau hierarkis (misalnya , , , dan lain sebagainya): tweet → user tweet → media media → author Haruskah kita hanya mengembalikan daftar tweet terlebih dahulu dan kemudian mengambil entitas dependen (seperti rincian pengguna) dalam beberapa kueri terpisah sesuai permintaan? Atau haruskah kita mengembalikan semua data sekaligus, sehingga menambah waktu dan ukuran pemuatan pertama, tetapi menghemat waktu untuk semua panggilan berikutnya? Apakah kita perlu menormalkan data dalam kasus ini untuk mengurangi ukuran muatan (misalnya ketika pengguna yang sama menjadi penulis banyak tweet dan kita ingin menghindari pengulangan data pengguna berulang kali dalam setiap entitas tweet)? Atau sebaiknya merupakan kombinasi dari pendekatan di atas? Mari kita lihat bagaimana X menanganinya. Sebelumnya pada tipe subtipe digunakan. Mari kita lihat bagaimana tampilannya: 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[]; }; Yang menarik di sini adalah sebagian besar data dependen seperti dan disematkan ke dalam respons pada panggilan pertama (tidak ada kueri berikutnya). tweet → media tweet → author Selain itu, koneksi dan dengan entitas tidak dinormalisasi (jika dua tweet memiliki penulis yang sama, datanya akan diulang di setiap objek tweet). Namun tampaknya hal itu tidak masalah, karena dalam lingkup linimasa beranda untuk pengguna tertentu, tweet akan ditulis oleh banyak penulis dan pengulangan mungkin terjadi tetapi jarang. User Media Tweet Asumsi saya adalah bahwa API (yang tidak kami bahas di sini), yang bertanggung jawab untuk mengambil tweet dari akan menanganinya secara berbeda, tetapi, tampaknya, bukan itu masalahnya. mengembalikan daftar tweet dari pengguna yang sama dan menyematkan data pengguna yang sama berulang-ulang untuk setiap tweet. Ini menarik. Mungkin kesederhanaan pendekatan ini mengalahkan beberapa overhead ukuran data (mungkin data pengguna dianggap cukup kecil ukurannya). Saya tidak yakin. UserTweets satu pengguna tertentu UserTweets Pengamatan lain tentang hubungan entitas adalah bahwa entitas juga memiliki tautan ke (penulis). Namun, hal itu tidak dilakukan melalui penyematan entitas secara langsung seperti yang dilakukan entitas , melainkan melalui properti . Media User Tweet Media.source_user_id_str "Komentar" (yang juga merupakan "tweet" menurut sifatnya) untuk setiap "tweet" di linimasa beranda tidak diambil sama sekali. Untuk melihat utas tweet, pengguna harus mengeklik tweet untuk melihat tampilan terperincinya. Utas tweet akan diambil dengan memanggil titik akhir (lebih lanjut tentangnya di bagian "Halaman detail tweet" di bawah). TweetDetail Entitas lain yang dimiliki setiap adalah (yaitu "Rekomendasikan lebih jarang" atau "Lihat lebih sedikit"). Cara disimpan dalam objek respons berbeda dari cara objek dan disimpan. Sementara entitas dan merupakan bagian dari , disimpan secara terpisah dalam array dan ditautkan melalui . Itu sedikit mengejutkan bagi saya karena tampaknya tidak ada tindakan yang dapat digunakan kembali. Sepertinya satu tindakan digunakan untuk satu tweet tertentu saja. Jadi sepertinya dapat disematkan ke setiap tweet dengan cara yang sama seperti entitas . Namun, saya mungkin melewatkan beberapa kerumitan tersembunyi di sini (seperti fakta bahwa setiap tindakan dapat memiliki tindakan turunan). Tweet FeedbackActions FeedbackActions User Media User Media Tweet FeedbackActions TimelineItem.content.feedbackInfo.feedbackKeys ActionKey FeedbackActions Media Rincian lebih lanjut tentang tindakan tersebut ada di bagian "Tindakan Tweet" di bawah. Penyortiran Urutan penyortiran entri garis waktu ditentukan oleh backend melalui properti : 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', }; }; itu sendiri mungkin terlihat seperti ini . Kemungkinan besar itu berhubungan langsung dengan atau berasal dari . sortIndex '1867231621095096312' ID Kepingan Salju . Sebenarnya sebagian besar ID yang Anda lihat dalam respons (ID tweet) mengikuti konvensi "ID Snowflake" dan terlihat seperti '1867231621095096312' Jika ini digunakan untuk mengurutkan entitas seperti tweet, sistem memanfaatkan pengurutan kronologis bawaan ID Snowflake. Tweet atau objek dengan nilai sortIndex yang lebih tinggi (stempel waktu yang lebih baru) muncul lebih tinggi di feed, sedangkan yang memiliki nilai lebih rendah (stempel waktu yang lebih lama) muncul lebih rendah di feed. Berikut ini adalah decoding ID Snowflake (dalam kasus kami ) langkah demi langkah: sortIndex 1867231621095096312 Ekstrak : Cap Waktu Cap waktu diperoleh dengan menggeser ke kanan ID Snowflake sebanyak 22 bit (untuk menghilangkan 22 bit bagian bawah untuk pusat data, ID pekerja, dan urutan): 1867231621095096312 → 445182709954 Tambahkan : Epoch Twitter Menambahkan epoch khusus Twitter (1288834974657) ke stempel waktu ini memberikan stempel waktu UNIX dalam milidetik: 445182709954 + 1288834974657 → 1734017684611ms Ubah ke : tanggal yang dapat dibaca manusia Mengonversi cap waktu UNIX ke datetime UTC memberikan: 1734017684611ms → 2024-12-12 15:34:44.611 (UTC) Jadi kita dapat berasumsi di sini bahwa tweet di timeline beranda diurutkan secara kronologis. Tindakan tweet Setiap tweet memiliki menu "Tindakan". Tindakan untuk setiap tweet berasal dari backend dalam array dan ditautkan dengan tweet melalui : 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' }; }; Yang menarik di sini adalah bahwa rangkaian tindakan yang datar ini sebenarnya adalah pohon (atau grafik? Saya tidak memeriksa), karena setiap tindakan mungkin memiliki tindakan turunan (lihat rangkaian ). Ini masuk akal, misalnya, ketika setelah pengguna mengklik tindakan , tindak lanjutnya mungkin menunjukkan tindakan , sebagai cara menjelaskan mengapa pengguna tidak menyukai tweet tersebut. TimelineAction.value.childKeys "Tidak Suka" "Posting ini tidak relevan" Halaman detail Tweet Setelah pengguna ingin melihat halaman detail tweet (yaitu untuk melihat rangkaian komentar/tweet), pengguna mengklik tweet tersebut dan permintaan ke titik akhir berikut dilakukan: 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} Saya penasaran mengapa daftar tweet diambil melalui panggilan , tetapi setiap detail tweet diambil melalui panggilan . Tampaknya tidak konsisten. Terutama mengingat bahwa parameter kueri serupa seperti , , dan lainnya kali ini dilewatkan di URL dan bukan di badan permintaan. Format respons juga serupa dan menggunakan kembali tipe dari panggilan daftar. Saya tidak yakin mengapa demikian. Namun sekali lagi, saya yakin saya mungkin melewatkan beberapa kerumitan latar belakang di sini. POST GET query-id features Berikut ini adalah tipe badan respons yang disederhanakan: 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', }; }; Responsnya cukup mirip (dalam jenisnya) dengan respons daftar, jadi kita tidak akan membahasnya terlalu lama di sini. Satu hal yang menarik adalah bahwa "komentar" (atau percakapan) dari setiap tweet sebenarnya adalah tweet lain (lihat tipe ). Jadi, utas tweet terlihat sangat mirip dengan umpan timeline beranda dengan menampilkan daftar entri . Ini terlihat elegan. Contoh yang bagus dari pendekatan universal dan dapat digunakan kembali untuk desain API. TimelineModule TimelineTweet Menyukai tweet tersebut Ketika pengguna menyukai tweet, permintaan ke titik akhir berikut sedang dilakukan: POST POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet Berikut ini adalah tipe badan : permintaan type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N61twFgted2EgXILM7A' }; Berikut ini adalah tipe badan : respon type FavoriteTweetResponse = { data: { favorite_tweet: 'Done', } } Tampak lugas dan juga menyerupai pendekatan RPC pada desain API. Kesimpulan Kami telah menyentuh beberapa bagian dasar dari desain API linimasa beranda dengan melihat contoh API X. Saya membuat beberapa asumsi selama proses tersebut sejauh pengetahuan saya. Saya yakin beberapa hal mungkin telah saya tafsirkan secara salah dan saya mungkin telah melewatkan beberapa nuansa yang rumit. Namun, meskipun demikian, saya harap Anda memperoleh beberapa wawasan yang berguna dari ikhtisar tingkat tinggi ini, sesuatu yang dapat Anda terapkan dalam sesi Desain API berikutnya. Awalnya, saya berencana untuk mengunjungi situs web teknologi papan atas serupa untuk mendapatkan beberapa wawasan dari Facebook, Reddit, YouTube, dan lainnya serta mengumpulkan praktik terbaik dan solusi yang telah teruji. Saya tidak yakin apakah saya akan punya waktu untuk melakukannya. Kita lihat saja nanti. Namun, ini bisa menjadi latihan yang menarik. Lampiran: Semua jenis di satu tempat Sebagai referensi, saya menambahkan semua jenis sekaligus di sini. Anda juga dapat menemukan semua jenis di mengajukan. jenis/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;