Merge branch 'main' of https://github.com/govdbot/govd
All checks were successful
Build and deploy / build-and-push-image (push) Successful in 8m44s
All checks were successful
Build and deploy / build-and-push-image (push) Successful in 8m44s
This commit is contained in:
commit
d565396c28
3 changed files with 122 additions and 97 deletions
|
@ -16,8 +16,8 @@ import (
|
|||
|
||||
const (
|
||||
apiHostname = "x.com"
|
||||
apiEndpoint = "https://x.com/i/api/graphql/zZXycP0V6H7m-2r0mOnFcA/TweetDetail"
|
||||
transactionID = "H/HJB3naILIqzncBBvY50XFL36IYeol67HU4ZlUe8wYvWdn9q7KJf7k2UBKOMwliRmCnohzCodsUCuvWOl9t0Z/wVY3QHA"
|
||||
apiBase = "https://" + apiHostname + "/i/api/graphql/"
|
||||
apiEndpoint = apiBase + "2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId"
|
||||
)
|
||||
|
||||
var ShortExtractor = &models.Extractor{
|
||||
|
@ -180,11 +180,17 @@ func GetTweetAPI(
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
tweet, err := FindTweetData(&apiResponse, tweetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tweet data: %w", err)
|
||||
result := apiResponse.Data.TweetResult.Result
|
||||
if result == nil {
|
||||
return nil, errors.New("failed to get tweet result")
|
||||
}
|
||||
var tweet *Tweet
|
||||
if result.Tweet != nil {
|
||||
tweet = result.Tweet
|
||||
} else if result.Legacy != nil {
|
||||
tweet = result.Legacy
|
||||
} else {
|
||||
return nil, errors.New("failed to get tweet data")
|
||||
}
|
||||
|
||||
return tweet, nil
|
||||
}
|
||||
|
|
|
@ -2,20 +2,9 @@ package twitter
|
|||
|
||||
type APIResponse struct {
|
||||
Data struct {
|
||||
ThreadedConversationWithInjectionsV2 struct {
|
||||
Instructions []struct {
|
||||
Entries []struct {
|
||||
EntryID string `json:"entryId"`
|
||||
Content struct {
|
||||
ItemContent struct {
|
||||
TweetResults struct {
|
||||
Result TweetResult `json:"result"`
|
||||
} `json:"tweet_results"`
|
||||
} `json:"itemContent"`
|
||||
} `json:"content"`
|
||||
} `json:"entries"`
|
||||
} `json:"instructions"`
|
||||
} `json:"threaded_conversation_with_injections_v2"`
|
||||
TweetResult struct {
|
||||
Result *TweetResult `json:"result,omitempty"`
|
||||
} `json:"tweetResult"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
|
@ -24,11 +13,29 @@ type TweetResult struct {
|
|||
Legacy *Tweet `json:"legacy,omitempty"`
|
||||
RestID string `json:"rest_id,omitempty"`
|
||||
Core *Core `json:"core,omitempty"`
|
||||
Views *ViewsInfo `json:"views,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
EditControl *EditInfo `json:"edit_control,omitempty"`
|
||||
TypeName string `json:"__typename,omitempty"`
|
||||
}
|
||||
|
||||
type EditInfo struct {
|
||||
EditTweetIDs []string `json:"edit_tweet_ids,omitempty"`
|
||||
EditableUntil string `json:"editable_until_msecs,omitempty"`
|
||||
IsEditEligible bool `json:"is_edit_eligible,omitempty"`
|
||||
EditsRemaining string `json:"edits_remaining,omitempty"`
|
||||
}
|
||||
|
||||
type ViewsInfo struct {
|
||||
Count string `json:"count,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
type Core struct {
|
||||
UserResults struct {
|
||||
Result struct {
|
||||
TypeName string `json:"__typename,omitempty"`
|
||||
RestID string `json:"rest_id,omitempty"`
|
||||
Legacy *UserLegacy `json:"legacy,omitempty"`
|
||||
} `json:"result"`
|
||||
} `json:"user_results"`
|
||||
|
@ -37,6 +44,8 @@ type Core struct {
|
|||
type UserLegacy struct {
|
||||
ScreenName string `json:"screen_name"`
|
||||
Name string `json:"name"`
|
||||
ProfileImageURLHTTPS string `json:"profile_image_url_https,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
type Tweet struct {
|
||||
|
@ -45,6 +54,15 @@ type Tweet struct {
|
|||
Entities *ExtendedEntities `json:"entities,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID string `json:"id_str"`
|
||||
BookmarkCount int `json:"bookmark_count,omitempty"`
|
||||
FavoriteCount int `json:"favorite_count,omitempty"`
|
||||
ReplyCount int `json:"reply_count,omitempty"`
|
||||
RetweetCount int `json:"retweet_count,omitempty"`
|
||||
QuoteCount int `json:"quote_count,omitempty"`
|
||||
PossiblySensitive bool `json:"possibly_sensitive,omitempty"`
|
||||
ConversationID string `json:"conversation_id_str,omitempty"`
|
||||
Lang string `json:"lang,omitempty"`
|
||||
UserIDStr string `json:"user_id_str,omitempty"`
|
||||
}
|
||||
|
||||
type ExtendedEntities struct {
|
||||
|
@ -56,7 +74,35 @@ type MediaEntity struct {
|
|||
MediaURLHTTPS string `json:"media_url_https"`
|
||||
ExpandedURL string `json:"expanded_url"`
|
||||
URL string `json:"url"`
|
||||
DisplayURL string `json:"display_url,omitempty"`
|
||||
IDStr string `json:"id_str,omitempty"`
|
||||
MediaKey string `json:"media_key,omitempty"`
|
||||
VideoInfo *VideoInfo `json:"video_info,omitempty"`
|
||||
Sizes *MediaSizes `json:"sizes,omitempty"`
|
||||
OriginalInfo *OriginalInfo `json:"original_info,omitempty"`
|
||||
MediaAvailability *MediaAvailability `json:"ext_media_availability,omitempty"`
|
||||
}
|
||||
|
||||
type MediaAvailability struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type MediaSizes struct {
|
||||
Large SizeInfo `json:"large,omitempty"`
|
||||
Medium SizeInfo `json:"medium,omitempty"`
|
||||
Small SizeInfo `json:"small,omitempty"`
|
||||
Thumb SizeInfo `json:"thumb,omitempty"`
|
||||
}
|
||||
|
||||
type SizeInfo struct {
|
||||
H int `json:"h"`
|
||||
W int `json:"w"`
|
||||
Resize string `json:"resize,omitempty"`
|
||||
}
|
||||
|
||||
type OriginalInfo struct {
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
}
|
||||
|
||||
type VideoInfo struct {
|
||||
|
|
|
@ -12,7 +12,6 @@ import (
|
|||
"govd/util"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const authToken = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
|
||||
|
@ -33,7 +32,6 @@ func BuildAPIHeaders(cookies []*http.Cookie) map[string]string {
|
|||
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",
|
||||
|
@ -47,44 +45,47 @@ func BuildAPIHeaders(cookies []*http.Cookie) map[string]string {
|
|||
}
|
||||
|
||||
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,
|
||||
variables := map[string]any{
|
||||
"tweetId": tweetID,
|
||||
"withCommunity": false,
|
||||
"includePromotedContent": false,
|
||||
"withVoice": false,
|
||||
}
|
||||
|
||||
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,
|
||||
features := map[string]any{
|
||||
"creator_subscriptions_tweet_preview_api_enabled": true,
|
||||
"tweetypie_unmention_optimization_enabled": true,
|
||||
"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": true,
|
||||
"responsive_web_edit_tweet_api_enabled": true,
|
||||
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
|
||||
"view_counts_everywhere_api_enabled": true,
|
||||
"longform_notetweets_consumption_enabled": true,
|
||||
"responsive_web_twitter_article_tweet_consumption_enabled": false,
|
||||
"tweet_awards_web_tipping_enabled": false,
|
||||
"freedom_of_speech_not_reach_fetch_enabled": true,
|
||||
"standardized_nudges_misinfo": true,
|
||||
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
|
||||
"longform_notetweets_rich_text_read_enabled": true,
|
||||
"longform_notetweets_inline_media_enabled": true,
|
||||
"responsive_web_graphql_exclude_directive_enabled": true,
|
||||
"verified_phone_label_enabled": false,
|
||||
"vibe_api_enabled": true,
|
||||
"responsive_web_media_download_video_enabled": false,
|
||||
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
|
||||
"responsive_web_graphql_timeline_navigation_enabled": true,
|
||||
"responsive_web_enhance_cards_enabled": false,
|
||||
}
|
||||
|
||||
fieldToggles := map[string]any{
|
||||
"withArticleRichContentState": false,
|
||||
}
|
||||
|
||||
variablesJSON, _ := sonic.ConfigFastest.Marshal(variables)
|
||||
featuresJSON, _ := sonic.ConfigFastest.Marshal(features)
|
||||
fieldTogglesJSON, _ := sonic.ConfigFastest.Marshal(fieldToggles)
|
||||
|
||||
return map[string]string{
|
||||
"variables": string(variablesJSON),
|
||||
"features": string(featuresJSON),
|
||||
"fieldToggles": string(fieldTogglesJSON),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,31 +137,3 @@ func extractResolution(url string) (int64, int64) {
|
|||
}
|
||||
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")
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue