summaryrefslogtreecommitdiffstats
path: root/gallery_dl/extractor/twitter.py
diff options
context:
space:
mode:
Diffstat (limited to 'gallery_dl/extractor/twitter.py')
-rw-r--r--gallery_dl/extractor/twitter.py119
1 files changed, 75 insertions, 44 deletions
diff --git a/gallery_dl/extractor/twitter.py b/gallery_dl/extractor/twitter.py
index c47021e..710bde3 100644
--- a/gallery_dl/extractor/twitter.py
+++ b/gallery_dl/extractor/twitter.py
@@ -295,6 +295,8 @@ class TwitterExtractor(Extractor):
tget("quoted_by_id_str")),
"reply_id" : text.parse_int(
tget("in_reply_to_status_id_str")),
+ "conversation_id": text.parse_int(
+ tget("conversation_id_str")),
"date" : date,
"author" : author,
"user" : self._user or author,
@@ -664,8 +666,8 @@ class TwitterSearchExtractor(TwitterExtractor):
subcategory = "search"
pattern = BASE_PATTERN + r"/search/?\?(?:[^&#]+&)*q=([^&#]+)"
test = ("https://twitter.com/search?q=nature", {
- "range": "1-40",
- "count": 40,
+ "range": "1-20",
+ "count": 20,
"archive": False,
})
@@ -1058,7 +1060,7 @@ class TwitterAPI():
def __init__(self, extractor):
self.extractor = extractor
- self.root = "https://api.twitter.com"
+ self.root = "https://twitter.com/i/api"
self._nsfw_warning = True
self._syndication = self.extractor.syndication
self._json_dumps = json.JSONEncoder(separators=(",", ":")).encode
@@ -1077,6 +1079,10 @@ class TwitterAPI():
auth_token = cookies.get("auth_token", domain=cookiedomain)
+ search = extractor.config("search-endpoint")
+ if search == "graphql" or not auth_token and search in ("auto", None):
+ self.search_adaptive = self.search_timeline
+
self.headers = {
"Accept": "*/*",
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejR"
@@ -1087,7 +1093,6 @@ class TwitterAPI():
"x-twitter-client-language": "en",
"x-twitter-active-user": "yes",
"x-csrf-token": csrf_token,
- "Origin": "https://twitter.com",
"Referer": "https://twitter.com/",
}
self.params = {
@@ -1131,47 +1136,44 @@ class TwitterAPI():
"enrichments,superFollowMetadata,unmentionInfo,editControl,"
"collab_control,vibe",
}
- self.variables = {
- "withDownvotePerspective": False,
- "withReactionsMetadata": False,
- "withReactionsPerspective": False,
- }
self.features = {
- "blue_business_profile_image_shape_enabled": False,
- "responsive_web_twitter_blue_verified_badge_is_enabled": True,
+ "hidden_profile_likes_enabled": False,
"responsive_web_graphql_exclude_directive_enabled": True,
"verified_phone_label_enabled": False,
- "responsive_web_graphql_skip_user_profile_"
- "image_extensions_enabled": False,
+ "subscriptions_verification_info_verified_since_enabled": True,
+ "highlights_tweets_tab_ui_enabled": True,
+ "creator_subscriptions_tweet_preview_api_enabled": True,
+ "responsive_web_graphql_"
+ "skip_user_profile_image_extensions_enabled": False,
"responsive_web_graphql_timeline_navigation_enabled": True,
}
self.features_pagination = {
- "blue_business_profile_image_shape_enabled": False,
- "responsive_web_twitter_blue_verified_badge_is_enabled": True,
+ "rweb_lists_timeline_redesign_enabled": True,
"responsive_web_graphql_exclude_directive_enabled": True,
"verified_phone_label_enabled": False,
+ "creator_subscriptions_tweet_preview_api_enabled": True,
"responsive_web_graphql_timeline_navigation_enabled": True,
"responsive_web_graphql_skip_user_profile_"
"image_extensions_enabled": False,
"tweetypie_unmention_optimization_enabled": True,
- "vibe_api_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,
"tweet_awards_web_tipping_enabled": False,
- "freedom_of_speech_not_reach_fetch_enabled": False,
+ "freedom_of_speech_not_reach_fetch_enabled": True,
"standardized_nudges_misinfo": True,
"tweet_with_visibility_results_prefer_gql_"
"limited_actions_policy_enabled": False,
"interactive_text_enabled": True,
"responsive_web_text_conversations_enabled": False,
- "longform_notetweets_richtext_consumption_enabled": False,
+ "longform_notetweets_rich_text_read_enabled": True,
+ "longform_notetweets_inline_media_enabled": False,
"responsive_web_enhance_cards_enabled": False,
}
def tweet_detail(self, tweet_id):
- endpoint = "/graphql/AV_lPTkN6Fc6LgerQpK8Zg/TweetDetail"
+ endpoint = "/graphql/JlLZj42Ltr2qwjasw-l5lQ/TweetDetail"
variables = {
"focalTweetId": tweet_id,
"referrer": "profile",
@@ -1179,9 +1181,7 @@ class TwitterAPI():
"includePromotedContent": True,
"withCommunity": True,
"withQuickPromoteEligibilityTweetFields": True,
- "withBirdwatchNotes": False,
- "withSuperFollowsUserFields": True,
- "withSuperFollowsTweetFields": True,
+ "withBirdwatchNotes": True,
"withVoice": True,
"withV2Timeline": True,
}
@@ -1189,7 +1189,7 @@ class TwitterAPI():
endpoint, variables, ("threaded_conversation_with_injections_v2",))
def user_tweets(self, screen_name):
- endpoint = "/graphql/BeHK76TOCY3P8nO-FWocjA/UserTweets"
+ endpoint = "/graphql/-AY51QoFpVf-w7TxjQ6lpw/UserTweets"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1201,7 +1201,7 @@ class TwitterAPI():
return self._pagination_tweets(endpoint, variables)
def user_tweets_and_replies(self, screen_name):
- endpoint = "/graphql/eZVlZu_1gwb6hMUDXBnZoQ/UserTweetsAndReplies"
+ endpoint = "/graphql/urrCZMyyIh1FkSFi2cdPUA/UserTweetsAndReplies"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1213,7 +1213,7 @@ class TwitterAPI():
return self._pagination_tweets(endpoint, variables)
def user_media(self, screen_name):
- endpoint = "/graphql/d_ONZLUHGCsErBCriRsLXg/UserMedia"
+ endpoint = "/graphql/lo965xQZdN2-eSM1Jc-W_A/UserMedia"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1246,7 +1246,7 @@ class TwitterAPI():
features=False)
def user_likes(self, screen_name):
- endpoint = "/graphql/fN4-E0MjFJ9Cn7IYConL7g/Likes"
+ endpoint = "/graphql/6JET1d0iHsIzW0Zjs3OOwQ/Likes"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1259,7 +1259,7 @@ class TwitterAPI():
return self._pagination_tweets(endpoint, variables)
def user_bookmarks(self):
- endpoint = "/graphql/RV1g3b8n_SGOHwkqKYSCFw/Bookmarks"
+ endpoint = "/graphql/YNtYqNuki6_oiVwx0uP8mQ/Bookmarks"
variables = {
"count": 100,
}
@@ -1270,7 +1270,7 @@ class TwitterAPI():
features=features)
def list_latest_tweets_timeline(self, list_id):
- endpoint = "/graphql/5DAiJG3bD77SiWEs4xViBw/ListLatestTweetsTimeline"
+ endpoint = "/graphql/ZBbXrl37E6za5ml-DIpmgg/ListLatestTweetsTimeline"
variables = {
"listId": list_id,
"count": 100,
@@ -1288,6 +1288,24 @@ class TwitterAPI():
params["spelling_corrections"] = "1"
return self._pagination_legacy(endpoint, params)
+ def search_timeline(self, query):
+ endpoint = "/graphql/7jT5GT59P8IFjgxwqnEdQw/SearchTimeline"
+ variables = {
+ "rawQuery": query,
+ "count": 20,
+ "product": "Latest",
+ "withDownvotePerspective": False,
+ "withReactionsMetadata": False,
+ "withReactionsPerspective": False,
+ }
+ features = self.features_pagination.copy()
+ features["blue_business_profile_image_shape_enabled"] = False
+ features["vibe_api_enabled"] = True
+ return self._pagination_tweets(
+ endpoint, variables,
+ ("search_by_raw_query", "search_timeline", "timeline"),
+ features=features)
+
def live_event_timeline(self, event_id):
endpoint = "/2/live_event/timeline/{}.json".format(event_id)
params = self.params.copy()
@@ -1305,11 +1323,10 @@ class TwitterAPI():
["twitter_objects"]["live_events"][event_id])
def list_by_rest_id(self, list_id):
- endpoint = "/graphql/D0EoyrDcct2MEqC-LnPzFg/ListByRestId"
+ endpoint = "/graphql/AmCdeFUvlrKAO96yHr-GCg/ListByRestId"
params = {
"variables": self._json_dumps({
"listId": list_id,
- "withSuperFollowsUserFields": True,
}),
"features": self._json_dumps(self.features),
}
@@ -1319,7 +1336,7 @@ class TwitterAPI():
raise exception.NotFoundError("list")
def list_members(self, list_id):
- endpoint = "/graphql/tzsIIbGUH9RyFCVmtO2W2w/ListMembers"
+ endpoint = "/graphql/a_ZQomd3MMk1crWkeiQBPg/ListMembers"
variables = {
"listId": list_id,
"count": 100,
@@ -1329,7 +1346,7 @@ class TwitterAPI():
endpoint, variables, ("list", "members_timeline", "timeline"))
def user_following(self, screen_name):
- endpoint = "/graphql/FaBzCqZXuQCb4PhB0RHqHw/Following"
+ endpoint = "/graphql/JPZiqKjET7_M1r5Tlr8pyA/Following"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1338,18 +1355,20 @@ class TwitterAPI():
return self._pagination_users(endpoint, variables)
def user_by_rest_id(self, rest_id):
- endpoint = "/graphql/S2BkcAyFMG--jef2N6Dgzw/UserByRestId"
+ endpoint = "/graphql/1YAM811Q8Ry4XyPpJclURQ/UserByRestId"
+ features = self.features.copy()
+ features["blue_business_profile_image_shape_enabled"] = True
params = {
"variables": self._json_dumps({
"userId": rest_id,
"withSafetyModeUserFields": True,
}),
- "features": self._json_dumps(self.features),
+ "features": self._json_dumps(features),
}
return self._call(endpoint, params)["data"]["user"]["result"]
def user_by_screen_name(self, screen_name):
- endpoint = "/graphql/k26ASEiniqy4eXMdknTSoQ/UserByScreenName"
+ endpoint = "/graphql/XA6F1nJELYg65hxOC2Ekmg/UserByScreenName"
params = {
"variables": self._json_dumps({
"screen_name": screen_name,
@@ -1380,7 +1399,9 @@ class TwitterAPI():
def _guest_token(self):
endpoint = "/1.1/guest/activate.json"
self.extractor.log.info("Requesting guest token")
- return str(self._call(endpoint, None, "POST", False)["guest_token"])
+ return str(self._call(
+ endpoint, None, "POST", False, "https://api.twitter.com",
+ )["guest_token"])
def _authenticate_guest(self):
guest_token = self._guest_token()
@@ -1389,8 +1410,8 @@ class TwitterAPI():
self.extractor.session.cookies.set(
"gt", guest_token, domain=self.extractor.cookiedomain)
- def _call(self, endpoint, params, method="GET", auth=True):
- url = self.root + endpoint
+ def _call(self, endpoint, params, method="GET", auth=True, root=None):
+ url = (root or self.root) + endpoint
while True:
if not self.headers["x-twitter-auth-type"] and auth:
@@ -1416,6 +1437,12 @@ class TwitterAPI():
self.extractor.wait(until=until, seconds=seconds)
continue
+ if response.status_code == 403 and \
+ not self.headers["x-twitter-auth-type"] and \
+ endpoint == "/2/search/adaptive.json":
+ raise exception.AuthorizationError(
+ "Login required to access search results")
+
# error
try:
data = response.json()
@@ -1524,7 +1551,6 @@ class TwitterAPI():
def _pagination_tweets(self, endpoint, variables,
path=None, stop_tweets=True, features=None):
extr = self.extractor
- variables.update(self.variables)
original_retweets = (extr.retweets == "original")
pinned_tweet = extr.pinned
@@ -1548,11 +1574,17 @@ class TwitterAPI():
instructions = instructions[key]
instructions = instructions["instructions"]
+ cursor = None
+ entries = None
for instr in instructions:
- if instr.get("type") == "TimelineAddEntries":
+ instr_type = instr.get("type")
+ if instr_type == "TimelineAddEntries":
entries = instr["entries"]
- break
- else:
+ elif instr_type == "TimelineReplaceEntry":
+ entry = instr["entry"]
+ if entry["entryId"].startswith("cursor-bottom-"):
+ cursor = entry["content"]["value"]
+ if entries is None:
raise KeyError()
except LookupError:
@@ -1581,7 +1613,7 @@ class TwitterAPI():
"Unable to retrieve Tweets from this timeline")
tweets = []
- tweet = cursor = None
+ tweet = None
if pinned_tweet:
pinned_tweet = False
@@ -1687,7 +1719,6 @@ class TwitterAPI():
variables["cursor"] = cursor
def _pagination_users(self, endpoint, variables, path=None):
- variables.update(self.variables)
params = {"variables": None,
"features" : self._json_dumps(self.features_pagination)}