package twitterscraper import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" ) type NewTweet struct { Text string Medias []*Media } type newTweet struct { Data struct { CreateTweet struct { TweetResults struct { Result result `json:"result"` } `json:"tweet_results"` } `json:"create_tweet"` } `json:"data"` } func (newTweet *newTweet) parse() *Tweet { r := &newTweet.Data.CreateTweet.TweetResults.Result t := &r.tweet if r.Typename == "TweetWithVisibilityResults" { t = &r.Tweet } if t.RestID != "" && t.Legacy.IDStr == "" { t.Legacy.IDStr = t.RestID } if t.NoteTweet.NoteTweetResults.Result.Text != "" { t.Legacy.FullText = t.NoteTweet.NoteTweetResults.Result.Text } var legacy *legacyTweet = &t.Legacy var user *legacyUser = &t.Core.UserResults.Result.Legacy tw := parseLegacyTweet(user, legacy) if tw == nil { return nil } if tw.Views == 0 && t.Views.Count != "" { tw.Views, _ = strconv.Atoi(t.Views.Count) } if t.QuotedStatusResult.Result != nil { tw.QuotedStatus = t.QuotedStatusResult.Result.parse() } return tw } func (s *Scraper) CreateTweet(tweet NewTweet) (*Tweet, error) { req, err := http.NewRequest("POST", "https://x.com/i/api/graphql/Qkq4oPdZYuNB_Qw3TDuFqQ/CreateTweet", nil) if err != nil { return nil, err } req.Header.Set("content-type", "application/json") media_entities := []map[string]interface{}{} if len(tweet.Medias) > 0 { for _, media := range tweet.Medias { media_entities = append(media_entities, map[string]interface{}{ "media_id": strconv.Itoa(media.ID), "tagged_users": []string{}, }) } } post_medias := map[string]interface{}{ "media_entities": media_entities, "possibly_sensitive": false, } variables := map[string]interface{}{ "dark_request": false, "media": post_medias, "semantic_annotation_ids": []string{}, "tweet_text": tweet.Text, } features := map[string]interface{}{ "communities_web_enable_tweet_community_results_fetch": true, "c9s_tweet_anatomy_moderator_badge_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, "longform_notetweets_rich_text_read_enabled": true, "longform_notetweets_inline_media_enabled": false, "articles_preview_enabled": true, "rweb_tipjar_consumption_enabled": false, "verified_phone_label_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, "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, "responsive_web_graphql_timeline_navigation_enabled": true, "responsive_web_enhance_cards_enabled": false, "premium_content_api_read_enabled": false, "responsive_web_grok_analyze_button_fetch_trends_enabled": false, "responsive_web_grok_analyze_post_followups_enabled": false, "responsive_web_jetfuel_frame": true, "responsive_web_grok_share_attachment_enabled": true, "responsive_web_grok_annotations_enabled": true, "content_disclosure_indicator_enabled": true, "content_disclosure_ai_generated_indicator_enabled": true, "responsive_web_grok_show_grok_translated_post": true, "responsive_web_grok_analysis_button_from_backend": true, "post_ctas_fetch_enabled": false, "profile_label_improvements_pcf_label_in_post_enabled": true, "responsive_web_profile_redirect_enabled": false, "rweb_cashtags_enabled": true, "responsive_web_grok_community_note_auto_translation_is_enabled": true, "responsive_web_grok_image_annotation_enabled": true, "responsive_web_grok_imagine_annotation_enabled": true, } body := map[string]interface{}{ "features": features, "variables": variables, "queryId": "Qkq4oPdZYuNB_Qw3TDuFqQ", } b, _ := json.Marshal(body) req.Body = io.NopCloser(bytes.NewReader(b)) var raw json.RawMessage err = s.RequestAPI(req, &raw) if err != nil { return nil, err } var response newTweet if err := json.Unmarshal(raw, &response); err != nil { return nil, fmt.Errorf("create tweet unmarshal: %w, raw: %s", err, raw) } if result := response.parse(); result != nil { return result, nil } return nil, fmt.Errorf("tweet wasn't post: %s", raw) } func (s *Scraper) DeleteTweet(tweetId string) error { req, err := s.newRequest("POST", "https://x.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet") if err != nil { return err } req.Header.Set("content-type", "application/json") variables := map[string]interface{}{ "dark_request": false, "tweet_id": tweetId, } body := map[string]interface{}{ "variables": variables, "queryId": "VaenaVgh5q5ih7kvyVjgtg", } b, _ := json.Marshal(body) req.Body = io.NopCloser(bytes.NewReader(b)) var response struct { Data struct { CreateTweet struct { TweetResults struct { } `json:"tweet_results"` } `json:"delete_tweet"` } `json:"data"` } err = s.RequestAPI(req, &response) if err != nil { return err } return nil } func (s *Scraper) CreateRetweet(tweetId string) (string, error) { req, err := s.newRequest("POST", "https://x.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet") if err != nil { return "", err } req.Header.Set("content-type", "application/json") variables := map[string]interface{}{ "dark_request": false, "tweet_id": tweetId, } body := map[string]interface{}{ "variables": variables, "queryId": "ojPdsZsimiJrUGLR1sjUtA", } b, _ := json.Marshal(body) req.Body = io.NopCloser(bytes.NewReader(b)) var response struct { Data struct { CreateRetweet struct { RetweetResults struct { Result struct { RestID string `json:"rest_id"` Legacy struct { FullText string `json:"full_text"` } `json:"legacy"` } `json:"result"` } `json:"retweet_results"` } `json:"create_retweet"` } `json:"data"` } err = s.RequestAPI(req, &response) if err != nil { return "", err } if response.Data.CreateRetweet.RetweetResults.Result.RestID != "" { return response.Data.CreateRetweet.RetweetResults.Result.RestID, nil } return "", errors.New("tweet wasn't retweeted") } // Retweeted tweets has their own id, but to delete retweet twitter using id of source tweet func (s *Scraper) DeleteRetweet(tweetId string) error { req, err := s.newRequest("POST", "https://x.com/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet") if err != nil { return err } req.Header.Set("content-type", "application/json") variables := map[string]interface{}{ "dark_request": false, "source_tweet_id": tweetId, } body := map[string]interface{}{ "variables": variables, "queryId": "iQtK4dl5hBmXewYZuEOKVw", } b, _ := json.Marshal(body) req.Body = io.NopCloser(bytes.NewReader(b)) var response struct { Data struct { Unretweet struct { SourceTweetResults struct { Result struct { RestID string `json:"rest_id"` Legacy struct { FullText string `json:"full_text"` } `json:"legacy"` } `json:"result"` } `json:"source_tweet_results"` } `json:"unretweet"` } `json:"data"` } err = s.RequestAPI(req, &response) if err != nil { return err } return nil } func (s *Scraper) LikeTweet(tweetId string) error { req, err := s.newRequest("POST", "https://x.com/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet") if err != nil { return err } req.Header.Set("content-type", "application/json") variables := map[string]interface{}{ "tweet_id": tweetId, } body := map[string]interface{}{ "variables": variables, "queryId": "lI07N6Otwv1PhnEgXILM7A", } b, _ := json.Marshal(body) req.Body = io.NopCloser(bytes.NewReader(b)) var response struct { Data struct { FavoriteTweet string `json:"favorite_tweet"` } `json:"data"` Errors []struct { Message string `json:"message"` Code int `json:"code"` } `json:"errors"` } err = s.RequestAPI(req, &response) if err != nil { return err } if len(response.Errors) > 0 && response.Errors[0].Code == 139 { return errors.New("tweet already liked") } if response.Data.FavoriteTweet != "Done" { return errors.New("unknown error") } return nil } func (s *Scraper) UnlikeTweet(tweetId string) error { req, err := s.newRequest("POST", "https://x.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet") if err != nil { return err } req.Header.Set("content-type", "application/json") variables := map[string]interface{}{ "tweet_id": tweetId, } body := map[string]interface{}{ "variables": variables, "queryId": "ZYKSe-w7KEslx3JhSIk5LA", } b, _ := json.Marshal(body) req.Body = io.NopCloser(bytes.NewReader(b)) var response struct { Data struct { UnfavoriteTweet string `json:"unfavorite_tweet"` } `json:"data"` Errors []struct { Message string `json:"message"` Code int `json:"code"` } `json:"errors"` } err = s.RequestAPI(req, &response) if err != nil { return err } if len(response.Errors) > 0 && response.Errors[0].Code == 144 { return errors.New("tweet already not liked") } if response.Data.UnfavoriteTweet != "Done" { return errors.New("unknown 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://x.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 }