From 18c6cccc11b19d86c3dffc5840d55469e630a476 Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Fri, 31 Jul 2026 23:26:37 -0300 Subject: [PATCH] feat: request full text, article, and media fields on post reads Expand tweet-read shortcuts (read, search, user posts, timeline, mentions, bookmarks, likes) so responses include text/note_tweet/ article content, author profile fields, and media attachments needed for rich post display. Add regression coverage for the query params. Fixes #91 --- api/shortcuts.go | 14 +++---- api/shortcuts_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) diff --git a/api/shortcuts.go b/api/shortcuts.go index 8fdac25..0726190 100644 --- a/api/shortcuts.go +++ b/api/shortcuts.go @@ -164,7 +164,7 @@ func ReadPost(client Client, postID string, opts RequestOptions) (json.RawMessag postID = ResolvePostID(postID) opts.Method = "GET" - opts.Endpoint = fmt.Sprintf("/2/tweets/%s?tweet.fields=created_at,public_metrics,conversation_id,in_reply_to_user_id,referenced_tweets,entities,attachments&expansions=author_id,referenced_tweets.id&user.fields=username,name,verified", postID) + opts.Endpoint = fmt.Sprintf("/2/tweets/%s?tweet.fields=created_at,public_metrics,conversation_id,in_reply_to_user_id,referenced_tweets,entities,attachments,text,note_tweet,article,author_id&expansions=author_id,referenced_tweets.id,attachments.media_keys,article.cover_media,article.media_entities&user.fields=username,name,verified,profile_image_url&media.fields=url,preview_image_url,type,variants", postID) opts.Data = "" return client.SendRequest(opts) @@ -178,7 +178,7 @@ func SearchPosts(client Client, query string, maxResults int, opts RequestOption maxResults = clampResults(maxResults, 10, 100) opts.Method = "GET" - opts.Endpoint = fmt.Sprintf("/2/tweets/search/recent?query=%s&max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name,verified", q, maxResults) + opts.Endpoint = fmt.Sprintf("/2/tweets/search/recent?query=%s&max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities,attachments,text,note_tweet,article,author_id&expansions=author_id,attachments.media_keys,article.cover_media,article.media_entities&user.fields=username,name,verified,profile_image_url&media.fields=url,preview_image_url,type,variants", q, maxResults) opts.Data = "" return client.SendRequest(opts) @@ -208,7 +208,7 @@ func LookupUser(client Client, username string, opts RequestOptions) (json.RawMe func GetUserPosts(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) { maxResults = clampResults(maxResults, 5, 100) opts.Method = "GET" - opts.Endpoint = fmt.Sprintf("/2/users/%s/tweets?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=referenced_tweets.id", userID, maxResults) + opts.Endpoint = fmt.Sprintf("/2/users/%s/tweets?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities,attachments,text,note_tweet,article,author_id&expansions=author_id,referenced_tweets.id,attachments.media_keys,article.cover_media,article.media_entities&user.fields=username,name,verified,profile_image_url&media.fields=url,preview_image_url,type,variants", userID, maxResults) opts.Data = "" return client.SendRequest(opts) @@ -219,7 +219,7 @@ func GetUserPosts(client Client, userID string, maxResults int, opts RequestOpti func GetTimeline(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) { maxResults = clampResults(maxResults, 1, 100) opts.Method = "GET" - opts.Endpoint = fmt.Sprintf("/2/users/%s/timelines/reverse_chronological?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name", userID, maxResults) + opts.Endpoint = fmt.Sprintf("/2/users/%s/timelines/reverse_chronological?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities,attachments,text,note_tweet,article,author_id&expansions=author_id,attachments.media_keys,article.cover_media,article.media_entities&user.fields=username,name,verified,profile_image_url&media.fields=url,preview_image_url,type,variants", userID, maxResults) opts.Data = "" return client.SendRequest(opts) @@ -229,7 +229,7 @@ func GetTimeline(client Client, userID string, maxResults int, opts RequestOptio func GetMentions(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) { maxResults = clampResults(maxResults, 5, 100) opts.Method = "GET" - opts.Endpoint = fmt.Sprintf("/2/users/%s/mentions?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities&expansions=author_id&user.fields=username,name", userID, maxResults) + opts.Endpoint = fmt.Sprintf("/2/users/%s/mentions?max_results=%d&tweet.fields=created_at,public_metrics,conversation_id,entities,attachments,text,note_tweet,article,author_id&expansions=author_id,attachments.media_keys,article.cover_media,article.media_entities&user.fields=username,name,verified,profile_image_url&media.fields=url,preview_image_url,type,variants", userID, maxResults) opts.Data = "" return client.SendRequest(opts) @@ -311,7 +311,7 @@ func Unbookmark(client Client, userID, postID string, opts RequestOptions) (json func GetBookmarks(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) { maxResults = clampResults(maxResults, 1, 100) opts.Method = "GET" - opts.Endpoint = fmt.Sprintf("/2/users/%s/bookmarks?max_results=%d&tweet.fields=created_at,public_metrics,entities&expansions=author_id&user.fields=username,name", userID, maxResults) + opts.Endpoint = fmt.Sprintf("/2/users/%s/bookmarks?max_results=%d&tweet.fields=created_at,public_metrics,entities,attachments,text,note_tweet,article,author_id&expansions=author_id,attachments.media_keys,article.cover_media,article.media_entities&user.fields=username,name,verified,profile_image_url&media.fields=url,preview_image_url,type,variants", userID, maxResults) opts.Data = "" return client.SendRequest(opts) @@ -387,7 +387,7 @@ func GetDMEvents(client Client, maxResults int, opts RequestOptions) (json.RawMe func GetLikedPosts(client Client, userID string, maxResults int, opts RequestOptions) (json.RawMessage, error) { maxResults = clampResults(maxResults, 5, 100) opts.Method = "GET" - opts.Endpoint = fmt.Sprintf("/2/users/%s/liked_tweets?max_results=%d&tweet.fields=created_at,public_metrics,entities&expansions=author_id&user.fields=username,name", userID, maxResults) + opts.Endpoint = fmt.Sprintf("/2/users/%s/liked_tweets?max_results=%d&tweet.fields=created_at,public_metrics,entities,attachments,text,note_tweet,article,author_id&expansions=author_id,attachments.media_keys,article.cover_media,article.media_entities&user.fields=username,name,verified,profile_image_url&media.fields=url,preview_image_url,type,variants", userID, maxResults) opts.Data = "" return client.SendRequest(opts) diff --git a/api/shortcuts_test.go b/api/shortcuts_test.go index 5df9b8d..c6a6693 100644 --- a/api/shortcuts_test.go +++ b/api/shortcuts_test.go @@ -224,6 +224,94 @@ func TestReadPost(t *testing.T) { assert.Equal(t, "123", result.Data.ID) } +func assertCSVContains(t *testing.T, csv string, values ...string) { + t.Helper() + padded := "," + csv + "," + for _, v := range values { + assert.Contains(t, padded, ","+v+",") + } +} + +func TestTweetReadEndpointsIncludeRequiredTweetFieldsAndMediaExpansions(t *testing.T) { + type callSpec struct { + name string + call func(client Client, opts RequestOptions) (json.RawMessage, error) + } + + tests := []callSpec{ + { + name: "ReadPost", + call: func(client Client, opts RequestOptions) (json.RawMessage, error) { + return ReadPost(client, "123", opts) + }, + }, + { + name: "SearchPosts", + call: func(client Client, opts RequestOptions) (json.RawMessage, error) { + return SearchPosts(client, "golang", 10, opts) + }, + }, + { + name: "GetUserPosts", + call: func(client Client, opts RequestOptions) (json.RawMessage, error) { + return GetUserPosts(client, "42", 10, opts) + }, + }, + { + name: "GetTimeline", + call: func(client Client, opts RequestOptions) (json.RawMessage, error) { + return GetTimeline(client, "42", 10, opts) + }, + }, + { + name: "GetMentions", + call: func(client Client, opts RequestOptions) (json.RawMessage, error) { + return GetMentions(client, "42", 10, opts) + }, + }, + { + name: "GetBookmarks", + call: func(client Client, opts RequestOptions) (json.RawMessage, error) { + return GetBookmarks(client, "42", 10, opts) + }, + }, + { + name: "GetLikedPosts", + call: func(client Client, opts RequestOptions) (json.RawMessage, error) { + return GetLikedPosts(client, "42", 10, opts) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var capturedQuery url.Values + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedQuery = r.URL.Query() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{}}`)) + })) + defer server.Close() + + client := shortcutClient(t, server) + _, err := tt.call(client, baseTestOpts()) + require.NoError(t, err) + + tweetFields := capturedQuery.Get("tweet.fields") + expansions := capturedQuery.Get("expansions") + userFields := capturedQuery.Get("user.fields") + mediaFields := capturedQuery.Get("media.fields") + + assertCSVContains(t, tweetFields, "text", "note_tweet", "article", "attachments", "public_metrics", "author_id", "entities", "created_at") + assertCSVContains(t, expansions, "author_id", "article.cover_media", "article.media_entities", "attachments.media_keys") + assertCSVContains(t, userFields, "username", "name", "verified", "profile_image_url") + assertCSVContains(t, mediaFields, "url", "preview_image_url", "type", "variants") + }) + } +} + // ---- SearchPosts ---- func TestSearchPosts(t *testing.T) {