twitter-scrapper/schedule.go

256 lines
6.4 KiB
Go
Raw Normal View History

2024-02-23 10:34:33 +03:00
package twitterscraper
import (
2024-02-23 10:58:24 +03:00
"bytes"
"encoding/json"
"errors"
"io"
2024-02-23 10:34:33 +03:00
"net/url"
2024-03-09 03:55:39 +03:00
"strconv"
2024-02-23 10:34:33 +03:00
"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"`
}
2024-03-09 03:55:39 +03:00
type TweetSchedule struct {
Text string
Date time.Time
Medias []*Media
}
2024-02-23 10:34:33 +03:00
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://x.com/i/api/graphql/ITtjAzvlZni2wWXwf295Qg/FetchScheduledTweets")
2024-02-23 10:34:33 +03:00
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
}
2024-02-23 10:58:24 +03:00
// DeleteScheduledTweet removes tweet from scheduled.
func (s *Scraper) DeleteScheduledTweet(id string) error {
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/CTOVqej0JBXAZSwkp1US0g/DeleteScheduledTweet")
2024-02-23 10:58:24 +03:00
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")
}
2024-02-23 11:24:34 +03:00
// CreateScheduledTweet schedule new tweet.
2024-03-09 03:55:39 +03:00
func (s *Scraper) CreateScheduledTweet(schedule TweetSchedule) (string, error) {
if schedule.Date.Unix() <= time.Now().Unix() {
2024-02-23 11:24:34 +03:00
return "", errors.New("date can't be in past")
}
req, err := s.newRequest("POST", "https://x.com/i/api/graphql/LCVzRQGxOaGnOnYH01NQXg/CreateScheduledTweet")
2024-02-23 11:24:34 +03:00
if err != nil {
return "", err
}
req.Header.Set("content-type", "application/json")
post_tweet_request := map[string]interface{}{
"auto_populate_reply_metadata": false,
2024-03-09 03:55:39 +03:00
"status": schedule.Text,
2024-02-23 11:24:34 +03:00
"exclude_reply_user_ids": []string{},
"media_ids": []string{},
}
2024-03-09 03:55:39 +03:00
if len(schedule.Medias) > 0 {
var media_ids []string
for _, media := range schedule.Medias {
media_ids = append(media_ids, strconv.Itoa(media.ID))
}
post_tweet_request["media_ids"] = media_ids
}
2024-02-23 11:24:34 +03:00
variables := map[string]interface{}{
"post_tweet_request": post_tweet_request,
2024-03-09 03:55:39 +03:00
"execute_at": schedule.Date.Unix(),
2024-02-23 11:24:34 +03:00
}
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")
}