2024-07-09 02:04:36 +03:00
|
|
|
package twitterscraper
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
2024-08-05 17:57:18 +03:00
|
|
|
"net/url"
|
2024-07-09 02:04:36 +03:00
|
|
|
"strconv"
|
2024-08-05 17:57:18 +03:00
|
|
|
"strings"
|
2024-07-09 02:04:36 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type NewTweet struct {
|
|
|
|
|
Text string
|
|
|
|
|
Medias []*Media
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-09 02:30:38 +03:00
|
|
|
type newTweet struct {
|
|
|
|
|
Data struct {
|
|
|
|
|
CreateTweet struct {
|
|
|
|
|
TweetResults struct {
|
|
|
|
|
Result tweet `json:"result"`
|
|
|
|
|
} `json:"tweet_results"`
|
|
|
|
|
} `json:"create_tweet"`
|
|
|
|
|
} `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (newTweet *newTweet) parse() *Tweet {
|
|
|
|
|
var tweet = &newTweet.Data.CreateTweet.TweetResults.Result
|
|
|
|
|
|
|
|
|
|
if tweet.NoteTweet.NoteTweetResults.Result.Text != "" {
|
|
|
|
|
tweet.Legacy.FullText = tweet.NoteTweet.NoteTweetResults.Result.Text
|
|
|
|
|
}
|
|
|
|
|
var legacy *legacyTweet = &tweet.Legacy
|
|
|
|
|
var user *legacyUser = &tweet.Core.UserResults.Result.Legacy
|
|
|
|
|
tw := parseLegacyTweet(user, legacy)
|
|
|
|
|
if tw == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if tw.Views == 0 && tweet.Views.Count != "" {
|
|
|
|
|
tw.Views, _ = strconv.Atoi(tweet.Views.Count)
|
|
|
|
|
}
|
|
|
|
|
if tweet.QuotedStatusResult.Result != nil {
|
|
|
|
|
tw.QuotedStatus = tweet.QuotedStatusResult.Result.parse()
|
|
|
|
|
}
|
|
|
|
|
return tw
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Scraper) CreateTweet(tweet NewTweet) (*Tweet, error) {
|
2026-05-21 18:54:10 +03:00
|
|
|
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/oB-5XsHNAbjvARJEc8CZFw/CreateTweet")
|
2024-07-09 02:04:36 +03:00
|
|
|
if err != nil {
|
2024-07-09 02:30:38 +03:00
|
|
|
return nil, err
|
2024-07-09 02:04:36 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.Header.Set("content-type", "application/json")
|
|
|
|
|
|
2024-07-09 02:54:38 +03:00
|
|
|
media_entities := []map[string]interface{}{}
|
2024-07-09 02:04:36 +03:00
|
|
|
|
|
|
|
|
if len(tweet.Medias) > 0 {
|
|
|
|
|
for _, media := range tweet.Medias {
|
2024-07-09 02:54:38 +03:00
|
|
|
media_entities = append(media_entities, map[string]interface{}{
|
|
|
|
|
"media_id": strconv.Itoa(media.ID),
|
|
|
|
|
"tagged_users": []string{},
|
|
|
|
|
})
|
2024-07-09 02:04:36 +03:00
|
|
|
}
|
2024-07-09 02:54:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
post_medias := map[string]interface{}{
|
|
|
|
|
"media_entities": media_entities,
|
|
|
|
|
"possibly_sensitive": false,
|
2024-07-09 02:04:36 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
"tweetypie_unmention_optimization_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,
|
|
|
|
|
"longform_notetweets_rich_text_read_enabled": true,
|
|
|
|
|
"longform_notetweets_inline_media_enabled": true,
|
|
|
|
|
"articles_preview_enabled": true,
|
|
|
|
|
"rweb_video_timestamps_enabled": true,
|
|
|
|
|
"rweb_tipjar_consumption_enabled": true,
|
|
|
|
|
"responsive_web_graphql_exclude_directive_enabled": true,
|
|
|
|
|
"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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body := map[string]interface{}{
|
|
|
|
|
"features": features,
|
|
|
|
|
"variables": variables,
|
|
|
|
|
"queryId": "oB-5XsHNAbjvARJEc8CZFw",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b, _ := json.Marshal(body)
|
|
|
|
|
req.Body = io.NopCloser(bytes.NewReader(b))
|
|
|
|
|
|
2024-07-09 02:30:38 +03:00
|
|
|
var response newTweet
|
2024-07-09 02:04:36 +03:00
|
|
|
err = s.RequestAPI(req, &response)
|
|
|
|
|
if err != nil {
|
2024-07-09 02:30:38 +03:00
|
|
|
return nil, err
|
2024-07-09 02:04:36 +03:00
|
|
|
}
|
|
|
|
|
|
2024-07-09 02:30:38 +03:00
|
|
|
if result := response.parse(); result != nil {
|
|
|
|
|
return result, nil
|
2024-07-09 02:04:36 +03:00
|
|
|
}
|
|
|
|
|
|
2024-07-09 02:30:38 +03:00
|
|
|
return nil, errors.New("tweet wasn't post")
|
2024-07-09 02:04:36 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Scraper) DeleteTweet(tweetId string) error {
|
2026-05-21 18:54:10 +03:00
|
|
|
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet")
|
2024-07-09 02:04:36 +03:00
|
|
|
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) {
|
2026-05-21 18:54:10 +03:00
|
|
|
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet")
|
2024-07-09 02:04:36 +03:00
|
|
|
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 {
|
2026-05-21 18:54:10 +03:00
|
|
|
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet")
|
2024-07-09 02:04:36 +03:00
|
|
|
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
|
|
|
|
|
}
|
2024-07-24 03:54:57 +03:00
|
|
|
|
|
|
|
|
func (s *Scraper) LikeTweet(tweetId string) error {
|
2026-05-21 18:54:10 +03:00
|
|
|
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet")
|
2024-07-24 03:54:57 +03:00
|
|
|
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 {
|
2026-05-21 18:54:10 +03:00
|
|
|
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet")
|
2024-07-24 03:54:57 +03:00
|
|
|
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
|
|
|
|
|
}
|
2024-08-05 17:57:18 +03:00
|
|
|
func (s *Scraper) GetTweetRetweeters(tweetId string, maxUsersNbr int, cursor string) ([]*Profile, string, error) {
|
|
|
|
|
if maxUsersNbr > 200 {
|
|
|
|
|
maxUsersNbr = 200
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 18:54:10 +03:00
|
|
|
req, err := s.newRequest("GET", "https://x.com/i/api/graphql/8019obfgnveiPiJuS2Rtow/Retweeters")
|
2024-08-05 17:57:18 +03:00
|
|
|
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
|
|
|
|
|
}
|