From a2938f52db7a1c02479cd5448e3ae910a895daca Mon Sep 17 00:00:00 2001 From: Valentine Date: Mon, 5 Aug 2024 17:57:18 +0300 Subject: [PATCH 1/2] add GetTweetRetweeters method --- timeline_v2.go | 31 +++++++++++++++++++++++ tweet.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ tweet_test.go | 16 ++++++++++++ 3 files changed, 114 insertions(+) diff --git a/timeline_v2.go b/timeline_v2.go index a4cebbf..2d7f10e 100644 --- a/timeline_v2.go +++ b/timeline_v2.go @@ -207,6 +207,37 @@ func (timeline *bookmarksTimelineV2) parseTweets() ([]*Tweet, string) { return tweets, cursor } +type retweetersTimelineV2 struct { + Data struct { + RetweetersTimeline struct { + Timeline struct { + Instructions []struct { + Type string `json:"type"` + Entries []entry `json:"entries"` + } `json:"instructions"` + } `json:"timeline"` + } `json:"retweeters_timeline"` + } `json:"data"` +} + +func (timeline *retweetersTimelineV2) parseUsers() ([]*Profile, string) { + var cursor string + var users []*Profile + for _, instruction := range timeline.Data.RetweetersTimeline.Timeline.Instructions { + for _, entry := range instruction.Entries { + if entry.Content.CursorType == "Bottom" { + cursor = entry.Content.Value + continue + } + if entry.Content.ItemContent.UserResults.Result.Typename == "User" { + user := entry.Content.ItemContent.UserResults.Result.parse() + users = append(users, &user) + } + } + } + return users, cursor +} + func (timeline *timelineV2) parseUsers() ([]*Profile, string) { var cursor string var users []*Profile diff --git a/tweet.go b/tweet.go index 0f31705..ae2549c 100644 --- a/tweet.go +++ b/tweet.go @@ -5,7 +5,9 @@ import ( "encoding/json" "errors" "io" + "net/url" "strconv" + "strings" ) type NewTweet struct { @@ -337,3 +339,68 @@ func (s *Scraper) UnlikeTweet(tweetId string) error { return nil } +func (s *Scraper) GetTweetRetweeters(tweetId string, maxUsersNbr int, cursor string) ([]*Profile, string, error) { + if maxUsersNbr > 200 { + maxUsersNbr = 200 + } + + req, err := s.newRequest("GET", "https://twitter.com/i/api/graphql/8019obfgnveiPiJuS2Rtow/Retweeters") + if err != nil { + return nil, "", err + } + + variables := map[string]interface{}{ + "tweetId": tweetId, + "includePromotedContent": false, + "count": maxUsersNbr, + } + + features := map[string]interface{}{ + "rweb_tipjar_consumption_enabled": true, + "responsive_web_graphql_exclude_directive_enabled": true, + "verified_phone_label_enabled": false, + "creator_subscriptions_tweet_preview_api_enabled": true, + "responsive_web_graphql_timeline_navigation_enabled": true, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, + "communities_web_enable_tweet_community_results_fetch": true, + "c9s_tweet_anatomy_moderator_badge_enabled": true, + "articles_preview_enabled": true, + "responsive_web_edit_tweet_api_enabled": true, + "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, + "view_counts_everywhere_api_enabled": true, + "longform_notetweets_consumption_enabled": true, + "responsive_web_twitter_article_tweet_consumption_enabled": true, + "tweet_awards_web_tipping_enabled": false, + "creator_subscriptions_quote_tweet_preview_enabled": false, + "freedom_of_speech_not_reach_fetch_enabled": true, + "standardized_nudges_misinfo": true, + "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true, + "rweb_video_timestamps_enabled": true, + "longform_notetweets_rich_text_read_enabled": true, + "longform_notetweets_inline_media_enabled": true, + "responsive_web_enhance_cards_enabled": false, + } + + if cursor != "" { + variables["cursor"] = cursor + } + + query := url.Values{} + query.Set("variables", mapToJSONString(variables)) + query.Set("features", mapToJSONString(features)) + req.URL.RawQuery = query.Encode() + + var timeline retweetersTimelineV2 + err = s.RequestAPI(req, &timeline) + if err != nil { + return nil, "", err + } + + users, nextCursor := timeline.parseUsers() + + if strings.HasPrefix(nextCursor, "0|") { + nextCursor = "" + } + + return users, nextCursor, nil +} diff --git a/tweet_test.go b/tweet_test.go index 981aa21..564c9f0 100644 --- a/tweet_test.go +++ b/tweet_test.go @@ -108,3 +108,19 @@ func TestLikeAndUnlikeTweet(t *testing.T) { t.Error(err) } } + +func TestGetTweetRetweeters(t *testing.T) { + if skipAuthTest { + t.Skip("Skipping test due to environment variable") + } + tweetId := "1792634158977568997" + + retweeters, _, err := testScraper.GetTweetRetweeters(tweetId, 20, "") + if err != nil { + t.Error(err) + } + + if len(retweeters) == 0 { + t.Error("0 tweet retweeters") + } +} From 171e94e5f517d04877a14226911d06d97697e8ea Mon Sep 17 00:00:00 2001 From: Valentine Date: Mon, 5 Aug 2024 18:05:02 +0300 Subject: [PATCH 2/2] add GetTweetRetweeters to readme --- CHANGELOG.md | 6 ++++++ README.md | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce1224..507d424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.0.11 + +05.08.2024 + +- Added method `GetTweetRetweeters` + ## v0.0.10 01.08.2024 diff --git a/README.md b/README.md index 9ff4b46..8f766af 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ You can use this library to get tweets, profiles, and trends trivially. - [Methods](#methods) - [Get tweet](#get-tweet) - [Get tweet replies](#get-tweet-replies) + - [Get tweet retweeters](#get-tweet-retweeters) - [Get user tweets](#get-user-tweets) - [Get user medias](#get-user-medias) - [Get bookmarks](#get-bookmarks) @@ -227,7 +228,7 @@ tweets, cursors, err := scraper.GetTweetReplies("1328684389388185600", cursor) To get all replies and replies of replies for tweet you can iterate for all cursors. To get only direct replies check if `cursor.ThreadID` is equal your tweet id. ```golang -tweets, cursors, err := testScraper.GetTweetReplies("1328684389388185600", "") +tweets, cursors, err := scraper.GetTweetReplies("1328684389388185600", "") if err != nil { panic(err) } @@ -236,7 +237,7 @@ for { if len(cursors) > 0 { var cursor *twitterscraper.ThreadCursor cursor, cursors = cursors[0], cursors[1:] - moreTweets, moreCursors, err := testScraper.GetTweetReplies(tweetId, cursor.Cursor) + moreTweets, moreCursors, err := scraper.GetTweetReplies(tweetId, cursor.Cursor) if err != nil { // you can check here if rate limited, await and repeat request panic(err) @@ -251,6 +252,17 @@ for { } ``` +### Get tweet retweeters + +500 requests / 15 minutes + +Returns a list of users who have retweeted the tweet. + +```golang +var cursor string +retweeters, cursor, err := scraper.GetTweetRetweeters("1328684389388185600", 20, cursor) +``` + ### Get user tweets 150 requests / 15 minutes