230 lines
5.3 KiB
Go
230 lines
5.3 KiB
Go
package twitterscraper
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
vidio "github.com/AlexEidt/Vidio"
|
|
)
|
|
|
|
type Media struct {
|
|
ID int
|
|
Type string
|
|
Size int
|
|
Parts int
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
type uploadInitResponse struct {
|
|
ID int `json:"media_id"`
|
|
ExpiresAfter int `json:"expires_after_secs"`
|
|
}
|
|
|
|
type ProcessingInfo struct {
|
|
State string `json:"state"`
|
|
CheckAfter int `json:"check_after_secs"`
|
|
Progress int `json:"progress_percent"`
|
|
}
|
|
|
|
type uploadStatusResponse struct {
|
|
ProcessingInfo ProcessingInfo `json:"processing_info"`
|
|
}
|
|
|
|
// Uploads photo, video or gif for further posting or scheduling. Expires in 24 hours if not used.
|
|
func (s *Scraper) UploadMedia(filePath string) (*Media, error) {
|
|
fileContent, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
media, err := s.uploadInit(filePath, fileContent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = s.uploadAppend(media, fileContent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var status *ProcessingInfo
|
|
|
|
status, err = s.uploadFinalize(media)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if strings.HasPrefix(media.Type, "image") {
|
|
return media, nil
|
|
}
|
|
|
|
for status.State != "succeeded" {
|
|
time.Sleep(2 * time.Second)
|
|
status, err = s.uploadStatus(media)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return media, nil
|
|
}
|
|
|
|
func (s *Scraper) uploadInit(filePath string, fileContent []byte) (*Media, error) {
|
|
var (
|
|
videoDuration float64
|
|
fileType string
|
|
mediaCategory = "tweet_"
|
|
)
|
|
|
|
fileType = http.DetectContentType(fileContent)
|
|
|
|
if fileType == "image/jpeg" || fileType == "image/png" {
|
|
mediaCategory += "image"
|
|
} else if fileType == "image/gif" {
|
|
mediaCategory += "gif"
|
|
} else if fileType == "video/mp4" || fileType == "video/quicktime" {
|
|
mediaCategory += "video"
|
|
|
|
video, err := vidio.NewVideo(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
videoDuration = video.Duration()
|
|
video.Close()
|
|
} else {
|
|
return nil, fmt.Errorf("file type %s unsupported by twitter, make sure you uploading photo, video or gif", fileType)
|
|
}
|
|
|
|
req, err := s.newRequest("POST", "https://upload.twitter.com/i/media/upload.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("command", "INIT")
|
|
query.Set("total_bytes", strconv.Itoa(len(fileContent)))
|
|
query.Set("media_type", fileType)
|
|
query.Set("media_category", mediaCategory)
|
|
if mediaCategory == "tweet_video" {
|
|
query.Set("video_duration_ms", strconv.FormatFloat(videoDuration*1000, 'f', -1, 64))
|
|
}
|
|
req.URL.RawQuery = query.Encode()
|
|
req.Header.Set("Origin", "https://twitter.com")
|
|
req.Header.Set("Referer", "https://twitter.com/")
|
|
|
|
var uploadInit uploadInitResponse
|
|
|
|
err = s.RequestAPI(req, &uploadInit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Media{
|
|
ID: uploadInit.ID,
|
|
Type: fileType,
|
|
Size: len(fileContent),
|
|
ExpiresAt: time.Now().Add(time.Duration(uploadInit.ExpiresAfter) * time.Second),
|
|
Parts: len(fileContent) / 2_000_000,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Scraper) uploadAppend(media *Media, fileContent []byte) error {
|
|
for i := 0; i <= media.Parts; i++ {
|
|
var partData []byte
|
|
|
|
if i+1 <= media.Parts {
|
|
partData = fileContent[i*2_000_000 : (i+1)*2_000_000]
|
|
} else {
|
|
partData = fileContent[i*2_000_000:]
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
w := multipart.NewWriter(&buf)
|
|
fw, err := w.CreateFormFile("media", "blob")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if _, err = io.Copy(fw, bytes.NewReader(partData)); err != nil {
|
|
return err
|
|
}
|
|
w.Close()
|
|
|
|
req, err := s.newRequest("POST", "https://upload.twitter.com/i/media/upload.json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("command", "APPEND")
|
|
query.Set("media_id", strconv.Itoa(media.ID))
|
|
query.Set("segment_index", strconv.Itoa(i))
|
|
req.URL.RawQuery = query.Encode()
|
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
|
req.Header.Set("Origin", "https://twitter.com")
|
|
req.Header.Set("Referer", "https://twitter.com/")
|
|
req.Body = io.NopCloser(&buf)
|
|
|
|
err = s.RequestAPI(req, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Scraper) uploadFinalize(media *Media) (*ProcessingInfo, error) {
|
|
req, err := s.newRequest("POST", "https://upload.twitter.com/i/media/upload.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("command", "FINALIZE")
|
|
query.Set("media_id", strconv.Itoa(media.ID))
|
|
query.Set("allow_async", "true")
|
|
req.URL.RawQuery = query.Encode()
|
|
req.Header.Set("Origin", "https://twitter.com")
|
|
req.Header.Set("Referer", "https://twitter.com/")
|
|
|
|
var response uploadStatusResponse
|
|
|
|
err = s.RequestAPI(req, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &response.ProcessingInfo, nil
|
|
}
|
|
|
|
func (s *Scraper) uploadStatus(media *Media) (*ProcessingInfo, error) {
|
|
req, err := s.newRequest("GET", "https://upload.twitter.com/i/media/upload.json")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
query := url.Values{}
|
|
query.Set("command", "STATUS")
|
|
query.Set("media_id", strconv.Itoa(media.ID))
|
|
req.URL.RawQuery = query.Encode()
|
|
req.Header.Set("Origin", "https://twitter.com")
|
|
req.Header.Set("Referer", "https://twitter.com/")
|
|
|
|
var response uploadStatusResponse
|
|
|
|
err = s.RequestAPI(req, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &response.ProcessingInfo, nil
|
|
}
|