238 lines
6.1 KiB
Go
238 lines
6.1 KiB
Go
package twitterscraper
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"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
|
|
}
|
|
|
|
// DeleteScheduledTweet removes tweet from scheduled.
|
|
func (s *Scraper) DeleteScheduledTweet(id string) error {
|
|
req, err := s.newRequest("POST", "https://twitter.com/i/api/graphql/CTOVqej0JBXAZSwkp1US0g/DeleteScheduledTweet")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("content-type", "application/json")
|
|
|
|
variables := map[string]interface{}{
|
|
"scheduled_tweet_id": id,
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"variables": variables,
|
|
"queryId": "CTOVqej0JBXAZSwkp1US0g",
|
|
}
|
|
|
|
b, _ := json.Marshal(body)
|
|
req.Body = io.NopCloser(bytes.NewReader(b))
|
|
|
|
var response struct {
|
|
Data struct {
|
|
Status string `json:"scheduledtweet_delete"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
err = s.RequestAPI(req, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if response.Data.Status == "Done" {
|
|
return nil
|
|
}
|
|
|
|
return errors.New("scheduled tweet wasn't removed")
|
|
}
|
|
|
|
// CreateScheduledTweet schedule new tweet.
|
|
func (s *Scraper) CreateScheduledTweet(text string, date time.Time) (string, error) {
|
|
if date.Unix() <= time.Now().Unix() {
|
|
return "", errors.New("date can't be in past")
|
|
}
|
|
|
|
req, err := s.newRequest("POST", "https://twitter.com/i/api/graphql/LCVzRQGxOaGnOnYH01NQXg/CreateScheduledTweet")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
req.Header.Set("content-type", "application/json")
|
|
|
|
post_tweet_request := map[string]interface{}{
|
|
"auto_populate_reply_metadata": false,
|
|
"status": text,
|
|
"exclude_reply_user_ids": []string{},
|
|
"media_ids": []string{},
|
|
}
|
|
|
|
variables := map[string]interface{}{
|
|
"post_tweet_request": post_tweet_request,
|
|
"execute_at": date.Unix(),
|
|
}
|
|
|
|
body := map[string]interface{}{
|
|
"variables": variables,
|
|
"queryId": "LCVzRQGxOaGnOnYH01NQXg",
|
|
}
|
|
|
|
b, _ := json.Marshal(body)
|
|
req.Body = io.NopCloser(bytes.NewReader(b))
|
|
|
|
var response struct {
|
|
Data struct {
|
|
Tweet struct {
|
|
ID string `json:"rest_id"`
|
|
} `json:"tweet"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
err = s.RequestAPI(req, &response)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if response.Data.Tweet.ID != "" {
|
|
return response.Data.Tweet.ID, nil
|
|
}
|
|
|
|
return "", errors.New("tweet wasn't scheduled")
|
|
}
|