paint-brush
Comprender a API de Twitter para que poidas deseñar a túa propiapor@trekhleb
194 lecturas Nova historia

Comprender a API de Twitter para que poidas deseñar a túa propia

por Oleksii Trekhleb22m2024/12/16
Read on Terminal Reader

Demasiado longo; Ler

Neste artigo, exploramos como se deseña a API da cronoloxía de X (Twitter) (x.com/home) e cales son os enfoques que utilizan para resolver varios desafíos.
featured image - Comprender a API de Twitter para que poidas deseñar a túa propia
Oleksii Trekhleb HackerNoon profile picture
0-item
1-item

Cando se trata de deseñar a API do sistema, os enxeñeiros de software adoitan considerar diferentes opcións como REST vs RPC vs GraphQL (ou outros enfoques híbridos) para determinar a mellor opción para unha tarefa ou proxecto específico.


Neste artigo, exploramos como está deseñada a API da liña temporal de inicio X ( Twitter ) (x.com/home) e que enfoques utilizan para resolver os seguintes desafíos:

  • Como obter a lista de chíos

  • Como facer unha ordenación e paxinación

  • Como devolver as entidades xerárquicas/vinculadas (chíos, usuarios, medios)

  • Como obter detalles do tweet

  • Como dar "me gusta" a un tweet


Só exploraremos estes desafíos a nivel de API, tratando a implementación do backend como unha caixa negra, xa que non temos acceso ao propio código do backend.


Mostrar aquí as solicitudes e respostas exactas pode ser complicado e difícil de seguir xa que os obxectos profundamente aniñados e repetitivos son difíciles de ler. Para facilitar a visualización da estrutura da carga útil de solicitude/resposta, intentei "escribir" a API da liña de tempo de inicio en TypeScript. Entón, cando se trata dos exemplos de solicitude/resposta, vou usar os tipos de solicitude e resposta en lugar de obxectos JSON reais. Ademais, lembre que os tipos simplifícanse e omítense moitas propiedades por motivos de brevidade.


Podes atopar todo tipo en tipos/x.ts ou na parte inferior deste artigo na sección "Apéndice: Todos os tipos nun só lugar".

Obtendo a lista de chíos

O punto final e a estrutura de solicitude/resposta

A obtención da lista de chíos para a liña de tempo de inicio comeza coa solicitude POST ao seguinte punto final:


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


Aquí tes un tipo de corpo de solicitude simplificado:


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


E aquí tes un tipo de corpo de resposta simplificado (afondaremos nos subtipos de resposta a continuación):


 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;


É interesante notar aquí, que "obter" os datos faise a través de "POSTing", que non é común para a API tipo REST, pero é común para unha API tipo GraphQL. Ademais, a parte graphql do URL indica que X está a usar o sabor GraphQL para a súa API.


Estou usando aquí a palabra "sabor" porque o propio corpo da solicitude non parece puro Consulta GraphQL , onde podemos describir a estrutura de resposta requirida, enumerando todas as propiedades que queremos obter:


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


A suposición aquí é que a API da liña de tempo doméstica non é unha API GraphQL pura, senón que é unha mestura de varios enfoques . Pasar os parámetros nunha solicitude POST como esta parece máis próximo á chamada RPC "funcional". Pero, ao mesmo tempo, parece que as funcións GraphQL poden usarse nalgún lugar do backend detrás do controlador/controlador do punto final de HomeTimeline . Unha mestura como esta tamén pode deberse a un código heredado ou a algún tipo de migración en curso. Pero de novo, estas son só as miñas especulacións.


Tamén podes notar que se usa o mesmo TimelineRequest.queryId no URL da API, así como no corpo da solicitude da API. Este queryId probablemente se xere no backend, despois incorpórase no paquete main.js e, a continuación, úsase cando se obteñen os datos do backend. É difícil para min entender como se usa exactamente este queryId xa que o backend de X é unha caixa negra no noso caso. Pero, de novo, a especulación aquí pode ser que, podería ser necesario para algún tipo de optimización de rendemento (¿reutilizar algúns resultados de consulta precalculados?), almacenamento en caché (relacionado con Apollo?), depuración (unir rexistros mediante queryId?), ou con fins de seguimento/rastrexo.

Tamén é interesante notar que TimelineResponse non contén unha lista de chíos, senón unha lista de instrucións , como "engadir un chío á liña de tempo" (consulte o tipo TimelineAddEntries ) ou "terminar a liña de tempo" (consulte a TimelineTerminateTimeline tipo).


A propia instrución TimelineAddEntries tamén pode conter diferentes tipos de entidades:

  • Chíos: consulta o tipo de TimelineItem
  • Cursores: vexa o tipo de TimelineCursor
  • Conversas/comentarios/fíos: consulte o tipo de TimelineModule


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


Isto é interesante desde o punto de vista da extensibilidade, xa que permite unha variedade máis ampla do que se pode renderizar na liña de tempo doméstica sen axustar demasiado a API.

Paxinación

A propiedade TimelineRequest.variables.count establece cantos chíos queremos obter á vez (por páxina). O valor predeterminado é 20. Non obstante, pódense devolver máis de 20 chíos na matriz TimelineAddEntries.entries . Por exemplo, a matriz pode conter 37 entradas para a carga da primeira páxina, porque inclúe chíos (29), chíos fixados (1), chíos promocionados (5) e cursores de paxinación (2). Non estou seguro de por que hai 29 chíos habituais co número solicitado de 20.


O TimelineRequest.variables.cursor é responsable da paxinación baseada no cursor.


" A paxinación do cursor úsase con máis frecuencia para datos en tempo real debido á frecuencia en que se engaden novos rexistros e porque ao ler os datos adoita ver primeiro os últimos resultados. Elimina a posibilidade de saltar elementos e mostrar o mesmo elemento máis dunha vez. En paxinación baseada no cursor, úsase un punteiro (ou cursor) constante para facer un seguimento de onde se deberían obter os seguintes elementos do conxunto de datos." Vexa o Paxinación compensada vs paxinación do cursor fío para o contexto.


Ao buscar a lista de chíos por primeira vez o TimelineRequest.variables.cursor está baleiro, xa que queremos obter os chíos principais da lista predeterminada (probablemente precalculada) de chíos personalizados.


Non obstante, na resposta, xunto cos datos do tweet, o backend tamén devolve as entradas do cursor. Aquí está a xerarquía do tipo de resposta: 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'; }; };


Cada páxina contén a lista de chíos xunto cos cursores "arriba" e "inferior":


Despois de cargar os datos da páxina, podemos ir desde a páxina actual en ambas direccións e buscar os chíos "anteriores/antigos" usando o cursor "inferior" ou os chíos "seguintes/mais novos" usando o cursor "superior". A miña suposición é que buscar os "seguintes" chíos usando o cursor "superior" ocorre en dous casos: cando os novos chíos foron engadidos mentres o usuario aínda está lendo a páxina actual, ou cando o usuario comeza a desprazar o feed cara arriba (e hai sen entradas almacenadas na caché ou se as entradas anteriores foron eliminadas por motivos de rendemento).


O propio cursor da X pode verse así: DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA . Nalgúns deseños de API, o cursor pode ser unha cadea codificada en Base64 que contén o ID da última entrada da lista ou a marca de tempo da última entrada vista. Por exemplo: eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} e, a continuación, estes datos úsanse para consultar a base de datos en consecuencia. No caso da API X, parece que o cursor está sendo decodificado en Base64 nunha secuencia binaria personalizada que pode requirir algunha descodificación adicional para sacarlle significado (é dicir, a través das definicións das mensaxes de Protobuf). Dado que non sabemos se é unha codificación .proto e tampouco coñecemos a definición da mensaxe .proto , podemos supoñer que o backend sabe como consultar o seguinte lote de chíos en función da cadea do cursor.


O parámetro TimelineResponse.variables.seenTweetIds úsase para informar ao servidor sobre os chíos da páxina activa actualmente do desprazamento infinito que xa viu o cliente. Isto probablemente axude a garantir que o servidor non inclúa chíos duplicados nas seguintes páxinas de resultados.

Entidades vinculadas/xerárquicas

Un dos retos que hai que resolver nas API como a liña de tempo de inicio (ou a fonte de inicio) é descubrir como devolver as entidades vinculadas ou xerárquicas (é dicir, tweet → user , tweet → media , media → author , etc.):


  • Só debemos devolver primeiro a lista de chíos e despois buscar as entidades dependentes (como os detalles do usuario) nunha serie de consultas separadas baixo demanda?
  • Ou deberíamos devolver todos os datos á vez, aumentando o tempo e o tamaño da primeira carga, pero aforrando o tempo para todas as chamadas posteriores?
    • Necesitamos normalizar os datos neste caso para reducir o tamaño da carga útil (é dicir, cando un mesmo usuario é autor de moitos chíos e queremos evitar que se repitan os datos do usuario unha e outra vez en cada entidade de chío)?
  • Ou debería ser unha combinación dos enfoques anteriores?


Vexamos como o manexa X.

Anteriormente no tipo TimelineTweet utilizouse o subtipo Tweet . A ver como queda:


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


O que é interesante aquí é que a maioría dos datos dependentes como tweet → media e tweet → author están incrustados na resposta na primeira chamada (sen consultas posteriores).


Ademais, non se normalizan as conexións User e Media coas entidades Tweet (se dous chíos teñen o mesmo autor, os seus datos repetiranse en cada obxecto de tweet). Pero parece que debería estar ben, xa que no ámbito da liña de tempo de inicio para un usuario específico os chíos serán escritos por moitos autores e as repeticións son posibles pero escasas.


A miña suposición era que a API UserTweets (que non tratamos aquí), que se encarga de buscar os chíos dun usuario en particular, o manexará de forma diferente, pero, ao parecer, non é o caso. UserTweets devolve a lista de chíos do mesmo usuario e incorpora os mesmos datos de usuario unha e outra vez para cada chío. É interesante. Quizais a simplicidade do enfoque supere algunha sobrecarga do tamaño dos datos (quizais os datos do usuario se consideren bastante pequenos). Non estou seguro.


Outra observación sobre a relación das entidades é que a entidade Media tamén ten unha ligazón ao User (o autor). Pero non o fai mediante a incorporación directa de entidades como fai a entidade Tweet , senón que enlaza a través da propiedade Media.source_user_id_str .


Os "comentarios" (que tamén son os "tweets" pola súa natureza) para cada "tweet" na liña de tempo de inicio non se obteñen en absoluto. Para ver o fío de chío o usuario debe facer clic no chío para ver a súa vista detallada. O fío de chío obterase chamando ao punto final TweetDetail (máis sobre iso na sección "Páxina de detalles do chío" a continuación).


Outra entidade que ten cada Tweet é FeedbackActions (é dicir, "Recomendar con menos frecuencia" ou "Ver menos"). A forma en que se almacenan as FeedbackActions no obxecto de resposta é diferente da forma en que se almacenan os obxectos User e Media . Aínda que as entidades User e Media forman parte do Tweet , as FeedbackActions gárdanse por separado na matriz TimelineItem.content.feedbackInfo.feedbackKeys e están ligadas a través da ActionKey . Foi unha lixeira sorpresa para min, xa que non parece ser o caso de que ningunha acción sexa reutilizable. Parece que unha acción só se usa para un chío en particular. Polo tanto, parece que as FeedbackActions poderían integrarse en cada tweet do mesmo xeito que as entidades Media . Pero quizais me falte algunha complexidade oculta aquí (como o feito de que cada acción pode ter accións fillos).

Máis detalles sobre as accións na sección "Accións de chíos" a continuación.

Ordenación

A orde de ordenación das entradas da liña de tempo está definida polo backend a través das propiedades 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', }; };


O propio sortIndex pode parecer algo así como '1867231621095096312' . Probablemente corresponda directamente a ou derive de a ID de copo de neve .


En realidade, a maioría dos ID que ves na resposta (ID de chío) seguen a convención de "ID de copo de neve" e parecen '1867231621095096312' .


Se isto se usa para ordenar entidades como tweets, o sistema aproveita a clasificación cronolóxica inherente dos ID de Snowflake. Os chíos ou obxectos cun valor de sortIndex máis alto (unha marca de tempo máis recente) aparecen máis altos no feed, mentres que os que teñen valores máis baixos (unha marca de tempo máis antiga) aparecen máis baixo no feed.


Aquí está a decodificación paso a paso do ID de copo de neve (no noso caso o sortIndex ) 1867231621095096312 :

  • Extrae a marca de tempo :
    • A marca de tempo derívase movendo á dereita o ID de Snowflake en 22 bits (para eliminar os 22 bits inferiores para o centro de datos, o ID do traballador e a secuencia): 1867231621095096312 → 445182709954
  • Engade a época de Twitter :
    • Engadir a época personalizada de Twitter (1288834974657) a esta marca de tempo dá a marca de tempo de UNIX en milisegundos: 445182709954 + 1288834974657 → 1734017684611ms
  • Converter nunha data lexible por humanos :
    • A conversión da marca de tempo de UNIX nunha data UTC dá: 1734017684611ms → 2024-12-12 15:34:44.611 (UTC)


Polo tanto, podemos supoñer aquí que os chíos na liña de tempo de inicio están ordenados cronoloxicamente.

Accións de chíos

Cada tweet ten un menú "Accións".


As accións de cada chío proceden do backend nunha matriz TimelineItem.content.feedbackInfo.feedbackKeys e están vinculadas cos chíos a través de 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' }; };


É interesante aquí que esta matriz plana de accións sexa en realidade unha árbore (ou un gráfico? Non comprobei), xa que cada acción pode ter accións fillas (consulte a matriz TimelineAction.value.childKeys ). Isto ten sentido, por exemplo, cando despois de que o usuario fai clic na acción "Non me gusta" , o seguimento pode ser mostrar a acción "Esta publicación non é relevante" , como unha forma de explicar por que o usuario non Non me gusta o tuit.

Páxina de detalles do chío

Unha vez que o usuario quere ver a páxina de detalles do tweet (é dicir, para ver o fío de comentarios/tweets), o usuario fai clic no tweet e realízase a solicitude GET ao seguinte punto final:


 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}


Tiven curiosidade aquí por que a lista de chíos se está a buscar a través da chamada POST , pero cada detalle do chío obtén a través da chamada GET . Parece inconsistente. Especialmente tendo en conta que parámetros de consulta similares, como query-id , features , e outros nesta ocasión pásanse no URL e non no corpo da solicitude. O formato de resposta tamén é semellante e está reutilizando os tipos da chamada de lista. Non estou seguro de por que é iso. Pero, de novo, estou seguro de que quizais me falte algunha complexidade de fondo aquí.

Aquí están os tipos de corpo de resposta simplificada:


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


A resposta é bastante similar (nos seus tipos) á resposta da lista, polo que non imos pasar moito tempo aquí.


Un matiz interesante é que os "comentarios" (ou conversas) de cada chío son en realidade outros chíos (consulta o tipo TimelineModule ). Polo tanto, o fío de chío parece moi similar ao feed da liña de tempo de inicio mostrando a lista de entradas de TimelineTweet . Isto parece elegante. Un bo exemplo dun enfoque universal e reutilizable para o deseño da API.

Gústalle o tuit

Cando a un usuario lle gusta o tweet, estase realizando a solicitude POST ao seguinte punto final:


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


Aquí están os tipos de corpo da solicitude :


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


Aquí están os tipos de corpo de resposta :


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


Parece sinxelo e tamén se asemella ao enfoque RPC para o deseño da API.


Conclusión

Tocamos algunhas partes básicas do deseño da API da liña de tempo na casa mirando o exemplo da API de X. Fixen algunhas suposicións ao longo do camiño, segundo o meu mellor coñecemento. Creo que algunhas cousas podería ter interpretado incorrectamente e podería ter perdido algúns matices complexos. Pero aínda tendo isto en conta, espero que teñas información útil desta visión xeral de alto nivel, algo que podes aplicar na túa próxima sesión de Deseño de API.


Inicialmente, tiña un plan para pasar por sitios similares de alta tecnoloxía para obter información de Facebook, Reddit, YouTube e outros e recoller as mellores prácticas e solucións probadas en batalla. Non estou seguro de atopar o tempo para facelo. Verá. Pero pode ser un exercicio interesante.

Apéndice: Todos os tipos nun só lugar

Para a referencia, estou engadindo todos os tipos dunha soa vez aquí. Tamén podes atopar todos os tipos tipos/x.ts arquivo.


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