we're aiming to automatically generate x-client-transaction-id header. due to a recent change on twitter backend, this header is now required. sadly, its not static, so would propably break again anytime soon
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
package twitter
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"govd/enums"
|
|
"govd/models"
|
|
"govd/util"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const authToken = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
|
|
|
|
var resolutionRegex = regexp.MustCompile(`(\d+)x(\d+)`)
|
|
|
|
func BuildAPIHeaders(cookies []*http.Cookie) map[string]string {
|
|
var csrfToken string
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "ct0" {
|
|
csrfToken = cookie.Value
|
|
break
|
|
}
|
|
}
|
|
if csrfToken == "" {
|
|
return nil
|
|
}
|
|
headers := map[string]string{
|
|
"authorization": "Bearer " + authToken,
|
|
"user-agent": util.ChromeUA,
|
|
"x-client-transaction-id": transactionID,
|
|
"x-twitter-auth-type": "OAuth2Session",
|
|
"x-twitter-client-language": "en",
|
|
"x-twitter-active-user": "yes",
|
|
}
|
|
|
|
if csrfToken != "" {
|
|
headers["x-csrf-token"] = csrfToken
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
func BuildAPIQuery(tweetID string) map[string]string {
|
|
variables := map[string]interface{}{
|
|
"focalTweetId": tweetID,
|
|
"includePromotedContent": true,
|
|
"with_rux_injections": false,
|
|
"withBirdwatchNotes": true,
|
|
"withCommunity": true,
|
|
"withDownvotePerspective": false,
|
|
"withQuickPromoteEligibilityTweetFields": true,
|
|
"withReactionsMetadata": false,
|
|
"withReactionsPerspective": false,
|
|
"withSuperFollowsTweetFields": true,
|
|
"withSuperFollowsUserFields": true,
|
|
"withV2Timeline": true,
|
|
"withVoice": true,
|
|
}
|
|
|
|
features := map[string]interface{}{
|
|
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": false,
|
|
"interactive_text_enabled": true,
|
|
"responsive_web_edit_tweet_api_enabled": true,
|
|
"responsive_web_enhance_cards_enabled": true,
|
|
"responsive_web_graphql_timeline_navigation_enabled": false,
|
|
"responsive_web_text_conversations_enabled": false,
|
|
"responsive_web_uc_gql_enabled": true,
|
|
"standardized_nudges_misinfo": true,
|
|
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": false,
|
|
"tweetypie_unmention_optimization_enabled": true,
|
|
"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": true,
|
|
"verified_phone_label_enabled": false,
|
|
"vibe_api_enabled": true,
|
|
}
|
|
|
|
variablesJSON, _ := sonic.ConfigFastest.Marshal(variables)
|
|
featuresJSON, _ := sonic.ConfigFastest.Marshal(features)
|
|
|
|
return map[string]string{
|
|
"variables": string(variablesJSON),
|
|
"features": string(featuresJSON),
|
|
}
|
|
}
|
|
|
|
func CleanCaption(caption string) string {
|
|
if caption == "" {
|
|
return ""
|
|
}
|
|
regex := regexp.MustCompile(`https?://t\.co/\S+`)
|
|
return strings.TrimSpace(regex.ReplaceAllString(caption, ""))
|
|
}
|
|
|
|
func ExtractVideoFormats(media *MediaEntity) ([]*models.MediaFormat, error) {
|
|
var formats []*models.MediaFormat
|
|
|
|
if media.VideoInfo == nil {
|
|
return formats, nil
|
|
}
|
|
|
|
duration := int64(media.VideoInfo.DurationMillis / 1000)
|
|
|
|
for _, variant := range media.VideoInfo.Variants {
|
|
if variant.ContentType == "video/mp4" {
|
|
width, height := extractResolution(variant.URL)
|
|
|
|
formats = append(formats, &models.MediaFormat{
|
|
Type: enums.MediaTypeVideo,
|
|
FormatID: fmt.Sprintf("mp4_%d", variant.Bitrate),
|
|
URL: []string{variant.URL},
|
|
VideoCodec: enums.MediaCodecAVC,
|
|
AudioCodec: enums.MediaCodecAAC,
|
|
Duration: duration,
|
|
Thumbnail: []string{media.MediaURLHTTPS},
|
|
Width: width,
|
|
Height: height,
|
|
Bitrate: int64(variant.Bitrate),
|
|
})
|
|
}
|
|
}
|
|
|
|
return formats, nil
|
|
}
|
|
|
|
func extractResolution(url string) (int64, int64) {
|
|
matches := resolutionRegex.FindStringSubmatch(url)
|
|
if len(matches) >= 3 {
|
|
width, _ := strconv.ParseInt(matches[1], 10, 64)
|
|
height, _ := strconv.ParseInt(matches[2], 10, 64)
|
|
return width, height
|
|
}
|
|
return 0, 0
|
|
}
|
|
|
|
func FindTweetData(resp *APIResponse, tweetID string) (*Tweet, error) {
|
|
instructions := resp.Data.ThreadedConversationWithInjectionsV2.Instructions
|
|
if len(instructions) == 0 {
|
|
return nil, errors.New("tweet data missing")
|
|
}
|
|
|
|
entries := instructions[0].Entries
|
|
entryID := "tweet-" + tweetID
|
|
|
|
for _, entry := range entries {
|
|
if entry.EntryID == entryID {
|
|
result := entry.Content.ItemContent.TweetResults.Result
|
|
|
|
if result.Tweet != nil {
|
|
return result.Tweet, nil
|
|
}
|
|
|
|
if result.Legacy != nil {
|
|
return result.Legacy, nil
|
|
}
|
|
|
|
return nil, errors.New("invalid tweet data")
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("tweet not found")
|
|
}
|