From a611486a08d636038706591c9b0055d832c21f9b Mon Sep 17 00:00:00 2001 From: Valentine Date: Fri, 9 Aug 2024 14:03:05 +0300 Subject: [PATCH 1/2] add GetProfileByID method --- CHANGELOG.md | 6 ++++ README.md | 9 +++++ profile.go | 89 +++++++++++++++++++++++++++++++++++++++++++------ profile_test.go | 11 ++++++ 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 507d424..7988c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.0.12 + +09.08.2024 + +- Added method `GetProfileByID` + ## v0.0.11 05.08.2024 diff --git a/README.md b/README.md index 8f766af..b402178 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ You can use this library to get tweets, profiles, and trends trivially. - [Search tweets](#search-tweets) - [Search params](#search-params) - [Get profile](#get-profile) + - [Get profile by id](#get-profile-by-id) - [Search profile](#search-profile) - [Get trends](#get-trends) - [Get following](#get-following) @@ -425,6 +426,14 @@ See [Rules and filtering](https://developer.twitter.com/en/docs/tweets/rules-and profile, err := scraper.GetProfile("taylorswift13") ``` +### Get profile by id + +95 requests / 15 minutes + +```golang +profile, err := scraper.GetProfileByID("17919972") +``` + ### Search profile > [!IMPORTANT] diff --git a/profile.go b/profile.go index 98e3517..631dfec 100644 --- a/profile.go +++ b/profile.go @@ -41,8 +41,10 @@ type Profile struct { type user struct { Data struct { User struct { - RestID string `json:"rest_id"` - Legacy legacyUser `json:"legacy"` + Result struct { + RestID string `json:"rest_id"` + Legacy legacyUser `json:"legacy"` + } `json:"result"` } `json:"user"` } `json:"data"` Errors []struct { @@ -53,18 +55,34 @@ type user struct { // GetProfile return parsed user profile. func (s *Scraper) GetProfile(username string) (Profile, error) { var jsn user - req, err := http.NewRequest("GET", "https://api.twitter.com/graphql/4S2ihIKfF3xhp-ENxvUAfQ/UserByScreenName", nil) + req, err := http.NewRequest("GET", "https://api.twitter.com/graphql/Yka-W8dz7RaEuQNkroPkYw/UserByScreenName", nil) if err != nil { return Profile{}, err } variables := map[string]interface{}{ - "screen_name": username, - "withHighlightedLabel": true, + "screen_name": username, + "withSafetyModeUserFields": true, + } + + features := map[string]interface{}{ + "hidden_profile_subscriptions_enabled": true, + "rweb_tipjar_consumption_enabled": true, + "responsive_web_graphql_exclude_directive_enabled": true, + "verified_phone_label_enabled": false, + "subscriptions_verification_info_is_identity_verified_enabled": true, + "subscriptions_verification_info_verified_since_enabled": true, + "highlights_tweets_tab_ui_enabled": true, + "responsive_web_twitter_article_notes_tab_enabled": true, + "subscriptions_feature_can_gift_premium": true, + "creator_subscriptions_tweet_preview_api_enabled": true, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, + "responsive_web_graphql_timeline_navigation_enabled": true, } query := url.Values{} query.Set("variables", mapToJSONString(variables)) + query.Set("features", mapToJSONString(features)) req.URL.RawQuery = query.Encode() err = s.RequestAPI(req, &jsn) @@ -72,20 +90,71 @@ func (s *Scraper) GetProfile(username string) (Profile, error) { return Profile{}, err } - if len(jsn.Errors) > 0 { + if len(jsn.Errors) > 0 && jsn.Data.User.Result.RestID == "" { return Profile{}, fmt.Errorf("%s", jsn.Errors[0].Message) } - if jsn.Data.User.RestID == "" { + if jsn.Data.User.Result.RestID == "" { return Profile{}, fmt.Errorf("rest_id not found") } - jsn.Data.User.Legacy.IDStr = jsn.Data.User.RestID + jsn.Data.User.Result.Legacy.IDStr = jsn.Data.User.Result.RestID - if jsn.Data.User.Legacy.ScreenName == "" { + if jsn.Data.User.Result.Legacy.ScreenName == "" { return Profile{}, fmt.Errorf("either @%s does not exist or is private", username) } - return parseProfile(jsn.Data.User.Legacy), nil + return parseProfile(jsn.Data.User.Result.Legacy), nil +} + +func (s *Scraper) GetProfileByID(userID string) (Profile, error) { + var jsn user + req, err := http.NewRequest("GET", "https://twitter.com/i/api/graphql/Qw77dDjp9xCpUY-AXwt-yQ/UserByRestId", nil) + if err != nil { + return Profile{}, err + } + + variables := map[string]interface{}{ + "userId": userID, + "withSafetyModeUserFields": true, + } + + features := map[string]interface{}{ + "hidden_profile_subscriptions_enabled": true, + "rweb_tipjar_consumption_enabled": true, + "responsive_web_graphql_exclude_directive_enabled": true, + "verified_phone_label_enabled": false, + "highlights_tweets_tab_ui_enabled": true, + "responsive_web_twitter_article_notes_tab_enabled": true, + "subscriptions_feature_can_gift_premium": true, + "creator_subscriptions_tweet_preview_api_enabled": true, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, + "responsive_web_graphql_timeline_navigation_enabled": true, + } + + query := url.Values{} + query.Set("variables", mapToJSONString(variables)) + query.Set("features", mapToJSONString(features)) + req.URL.RawQuery = query.Encode() + + err = s.RequestAPI(req, &jsn) + if err != nil { + return Profile{}, err + } + + if len(jsn.Errors) > 0 && jsn.Data.User.Result.RestID == "" { + return Profile{}, fmt.Errorf("%s", jsn.Errors[0].Message) + } + + if jsn.Data.User.Result.RestID == "" { + return Profile{}, fmt.Errorf("rest_id not found") + } + jsn.Data.User.Result.Legacy.IDStr = jsn.Data.User.Result.RestID + + if jsn.Data.User.Result.Legacy.ScreenName == "" { + return Profile{}, fmt.Errorf("either @%s does not exist or is private", userID) + } + + return parseProfile(jsn.Data.User.Result.Legacy), nil } // GetUserIDByScreenName from API diff --git a/profile_test.go b/profile_test.go index 2765c98..db426d8 100644 --- a/profile_test.go +++ b/profile_test.go @@ -135,6 +135,17 @@ func TestGetProfileErrorNotFound(t *testing.T) { } } +func TestGetProfileByID(t *testing.T) { + profile, err := testScraper.GetProfileByID("1221221876849995777") + if err != nil { + t.Error(err) + } + + if profile.Username != "tomdumont" { + t.Errorf("Expected username 'tomdumont', got '%s'", profile.Username) + } +} + func TestGetUserIDByScreenName(t *testing.T) { userID, err := testScraper.GetUserIDByScreenName("Twitter") if err != nil { From 3bfec2211f5d8c180f5e83d758dd6245a7bc3521 Mon Sep 17 00:00:00 2001 From: Valentine Date: Fri, 9 Aug 2024 14:21:56 +0300 Subject: [PATCH 2/2] fix error tests --- profile.go | 22 ++++++++++++++++++---- profile_test.go | 7 +++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/profile.go b/profile.go index 631dfec..3fc0661 100644 --- a/profile.go +++ b/profile.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "sync" "time" ) @@ -42,8 +43,9 @@ type user struct { Data struct { User struct { Result struct { - RestID string `json:"rest_id"` - Legacy legacyUser `json:"legacy"` + RestID string `json:"rest_id"` + Legacy legacyUser `json:"legacy"` + Message string `json:"message"` } `json:"result"` } `json:"user"` } `json:"data"` @@ -91,11 +93,17 @@ func (s *Scraper) GetProfile(username string) (Profile, error) { } if len(jsn.Errors) > 0 && jsn.Data.User.Result.RestID == "" { + if strings.Contains(jsn.Errors[0].Message, "Missing LdapGroup(visibility-custom-suspension)") { + return Profile{}, fmt.Errorf("user is suspended") + } return Profile{}, fmt.Errorf("%s", jsn.Errors[0].Message) } if jsn.Data.User.Result.RestID == "" { - return Profile{}, fmt.Errorf("rest_id not found") + if jsn.Data.User.Result.Message == "User is suspended" { + return Profile{}, fmt.Errorf("user is suspended") + } + return Profile{}, fmt.Errorf("user not found") } jsn.Data.User.Result.Legacy.IDStr = jsn.Data.User.Result.RestID @@ -142,11 +150,17 @@ func (s *Scraper) GetProfileByID(userID string) (Profile, error) { } if len(jsn.Errors) > 0 && jsn.Data.User.Result.RestID == "" { + if strings.Contains(jsn.Errors[0].Message, "Missing LdapGroup(visibility-custom-suspension)") { + return Profile{}, fmt.Errorf("user is suspended") + } return Profile{}, fmt.Errorf("%s", jsn.Errors[0].Message) } if jsn.Data.User.Result.RestID == "" { - return Profile{}, fmt.Errorf("rest_id not found") + if jsn.Data.User.Result.Message == "User is suspended" { + return Profile{}, fmt.Errorf("user is suspended") + } + return Profile{}, fmt.Errorf("user not found") } jsn.Data.User.Result.Legacy.IDStr = jsn.Data.User.Result.RestID diff --git a/profile_test.go b/profile_test.go index db426d8..eb82891 100644 --- a/profile_test.go +++ b/profile_test.go @@ -1,7 +1,6 @@ package twitterscraper_test import ( - "fmt" "strings" "testing" "time" @@ -116,15 +115,15 @@ func TestGetProfileErrorSuspended(t *testing.T) { if err == nil { t.Error("Expected Error, got success") } else { - if !strings.Contains(err.Error(), "Missing LdapGroup(visibility-custom-suspension)") { - t.Error("Expected error to contain 'Missing LdapGroup(visibility-custom-suspension)', got", err) + if !strings.Contains(err.Error(), "suspended") { + t.Error("Expected error to contain 'suspended', got", err) } } } func TestGetProfileErrorNotFound(t *testing.T) { neUser := "sample3123131" - expectedError := fmt.Sprintf("User '%s' not found", neUser) + expectedError := "user not found" _, err := testScraper.GetProfile(neUser) if err == nil { t.Error("Expected Error, got success")