diff --git a/schedule.go b/schedule.go new file mode 100644 index 0000000..aa7118a --- /dev/null +++ b/schedule.go @@ -0,0 +1,142 @@ +package twitterscraper + +import ( + "net/url" + "strings" + "time" +) + +type scheduleTweet struct { + RestID string `json:"rest_id"` + SchedulingInfo struct { + ExecuteAt int64 `json:"execute_at"` + State string `json:"state"` + } `json:"scheduling_info"` + TweetCreateRequest struct { + Type string `json:"type"` + Status string `json:"status"` + ExcludeReplyUserIds []interface{} `json:"exclude_reply_user_ids"` + MediaIds []interface{} `json:"media_ids"` + AutoPopulateReplyMetadata bool `json:"auto_populate_reply_metadata"` + } `json:"tweet_create_request"` + MediaEntities []struct { + MediaKey string `json:"media_key"` + MediaInfo struct { + Typename string `json:"__typename"` + OriginalImgURL string `json:"original_img_url"` + OriginalImgWidth int `json:"original_img_width"` + OriginalImgHeight int `json:"original_img_height"` + DurationMillis int `json:"duration_millis"` + Variants []struct { + ContentType string `json:"content_type"` + Bitrate int `json:"bit_rate,omitempty"` + URL string `json:"url"` + } `json:"variants"` + AspectRatio struct { + Numerator int `json:"numerator"` + Denominator int `json:"denominator"` + } `json:"aspect_ratio"` + PreviewImage struct { + OriginalImgURL string `json:"original_img_url"` + OriginalImgWidth int `json:"original_img_width"` + OriginalImgHeight int `json:"original_img_height"` + } `json:"preview_image"` + } `json:"media_info"` + } `json:"media_entities,omitempty"` +} + +func (result *scheduleTweet) parse() *ScheduledTweet { + tweet := &ScheduledTweet{ + ID: result.RestID, + State: result.SchedulingInfo.State, + ExecuteAt: time.Unix(result.SchedulingInfo.ExecuteAt/1000, 0), + Text: result.TweetCreateRequest.Status, + } + + for _, media := range result.MediaEntities { + k := strings.Split(media.MediaKey, "_") + key := k[len(k)-1] + + if media.MediaInfo.Typename == "ApiVideo" { + video := Video{ + ID: key, + Preview: media.MediaInfo.PreviewImage.OriginalImgURL, + } + + maxBitrate := 0 + for _, variant := range media.MediaInfo.Variants { + if variant.Bitrate > maxBitrate { + video.URL = strings.TrimSuffix(variant.URL, "?tag=10") + maxBitrate = variant.Bitrate + } + } + + tweet.Videos = append(tweet.Videos, video) + } else if media.MediaInfo.Typename == "ApiGif" { + gif := GIF{ + ID: key, + Preview: media.MediaInfo.PreviewImage.OriginalImgURL, + } + + maxBitrate := 0 + for _, variant := range media.MediaInfo.Variants { + if variant.Bitrate >= maxBitrate { + gif.URL = variant.URL + maxBitrate = variant.Bitrate + } + } + tweet.GIFs = append(tweet.GIFs, gif) + } else if media.MediaInfo.Typename == "ApiImage" { + tweet.Photos = append(tweet.Photos, Photo{ + ID: key, + URL: media.MediaInfo.OriginalImgURL, + }) + } + } + + return tweet +} + +type scheduleTweets struct { + Data struct { + Viewer struct { + ScheduledTweetList []scheduleTweet `json:"scheduled_tweet_list"` + } `json:"viewer"` + } `json:"data"` +} + +func (timeline *scheduleTweets) parseTweets() []*ScheduledTweet { + var tweets []*ScheduledTweet + + for _, entry := range timeline.Data.Viewer.ScheduledTweetList { + if tweet := entry.parse(); tweet != nil { + tweets = append(tweets, tweet) + } + } + return tweets +} + +// FetchScheduledTweets gets scheduled tweets via the Twitter frontend GraphQL API. +func (s *Scraper) FetchScheduledTweets() ([]*ScheduledTweet, error) { + req, err := s.newRequest("GET", "https://twitter.com/i/api/graphql/ITtjAzvlZni2wWXwf295Qg/FetchScheduledTweets") + if err != nil { + return nil, err + } + + variables := map[string]interface{}{ + "ascending": true, + } + + query := url.Values{} + query.Set("variables", mapToJSONString(variables)) + req.URL.RawQuery = query.Encode() + + var timeline scheduleTweets + err = s.RequestAPI(req, &timeline) + if err != nil { + return nil, err + } + + tweets := timeline.parseTweets() + return tweets, nil +} diff --git a/schedule_test.go b/schedule_test.go new file mode 100644 index 0000000..513fd01 --- /dev/null +++ b/schedule_test.go @@ -0,0 +1,12 @@ +package twitterscraper_test + +import ( + "testing" +) + +func TestFetchScheduledTweets(t *testing.T) { + _, err := testScraper.FetchScheduledTweets() + if err != nil { + t.Error(err) + } +} diff --git a/types.go b/types.go index 7a13c95..be34a0d 100644 --- a/types.go +++ b/types.go @@ -80,6 +80,16 @@ type ( Error error } + ScheduledTweet struct { + ID string + State string + ExecuteAt time.Time + Text string + Videos []Video + Photos []Photo + GIFs []GIF + } + legacyTweet struct { ConversationIDStr string `json:"conversation_id_str"` CreatedAt string `json:"created_at"`