aboutsummaryrefslogtreecommitdiffstats
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.py410
1 files changed, 269 insertions, 141 deletions
diff --git a/gallery_dl/extractor/twitter.py b/gallery_dl/extractor/twitter.py
index 4303524..c928507 100644
--- a/gallery_dl/extractor/twitter.py
+++ b/gallery_dl/extractor/twitter.py
@@ -16,6 +16,7 @@ import random
BASE_PATTERN = (r"(?:https?://)?(?:www\.|mobile\.)?"
r"(?:(?:[fv]x)?twitter|(?:fix(?:up|v))?x)\.com")
+USER_PATTERN = rf"{BASE_PATTERN}/([^/?#]+)"
class TwitterExtractor(Extractor):
@@ -47,8 +48,9 @@ class TwitterExtractor(Extractor):
self.cards_blacklist = self.config("cards-blacklist")
if not self.config("transform", True):
- self._transform_user = util.identity
- self._transform_tweet = util.identity
+ self._transform_community = \
+ self._transform_tweet = \
+ self._transform_user = util.identity
self._cursor = None
self._user = None
@@ -412,6 +414,11 @@ class TwitterExtractor(Extractor):
content = tget("full_text") or tget("text") or ""
entities = legacy["entities"]
+ if "author_community_relationship" in tweet:
+ tdata["community"] = self._transform_community(
+ tweet["author_community_relationship"]
+ ["community_results"]["result"])
+
if hashtags := entities.get("hashtags"):
tdata["hashtags"] = [t["text"] for t in hashtags]
@@ -453,6 +460,36 @@ class TwitterExtractor(Extractor):
return tdata
+ def _transform_community(self, com):
+ try:
+ cid = com.get("id_str") or com["rest_id"]
+ except KeyError:
+ return {}
+
+ try:
+ return self._user_cache[f"C#{cid}"]
+ except KeyError:
+ pass
+
+ self._user_cache[f"C#{cid}"] = cdata = {
+ "id": text.parse_int(cid),
+ "name": com["name"],
+ "description": com["description"],
+ "date": text.parse_timestamp(com["created_at"] // 1000),
+ "nsfw": com["is_nsfw"],
+ "role": com["role"],
+ "member_count": com["member_count"],
+ "rules": [rule["name"] for rule in com["rules"]],
+ "admin": (admin := com.get("admin_results")) and
+ admin["result"]["core"]["screen_name"], # noqa: E131
+ "creator": (creator := com.get("creator_results")) and
+ creator["result"]["core"]["screen_name"], # noqa: E131
+ "banner": (banner := com.get("custom_banner_media")) and
+ banner["media_info"]["original_img_url"], # noqa: E131
+ }
+
+ return cdata
+
def _transform_user(self, user):
try:
uid = user.get("rest_id") or user["id_str"]
@@ -465,35 +502,35 @@ class TwitterExtractor(Extractor):
except KeyError:
pass
- if "legacy" in user:
- user = user["legacy"]
+ core = user.get("core") or user
+ legacy = user.get("legacy") or user
+ lget = legacy.get
- uget = user.get
- if uget("withheld_scope"):
- self.log.warning("'%s'", uget("description"))
+ if lget("withheld_scope"):
+ self.log.warning("'%s'", lget("description"))
- entities = user["entities"]
+ entities = legacy["entities"]
self._user_cache[uid] = udata = {
"id" : text.parse_int(uid),
- "name" : user["screen_name"],
- "nick" : user["name"],
- "location" : uget("location"),
+ "name" : core["screen_name"],
+ "nick" : core["name"],
+ "location" : user["location"]["location"],
"date" : text.parse_datetime(
- uget("created_at"), "%a %b %d %H:%M:%S %z %Y"),
- "verified" : uget("verified", False),
- "protected" : uget("protected", False),
- "profile_banner" : uget("profile_banner_url", ""),
- "profile_image" : uget(
- "profile_image_url_https", "").replace("_normal.", "."),
- "favourites_count": uget("favourites_count"),
- "followers_count" : uget("followers_count"),
- "friends_count" : uget("friends_count"),
- "listed_count" : uget("listed_count"),
- "media_count" : uget("media_count"),
- "statuses_count" : uget("statuses_count"),
+ core["created_at"], "%a %b %d %H:%M:%S %z %Y"),
+ "verified" : user["verification"]["verified"],
+ "protected" : user["privacy"]["protected"],
+ "profile_banner" : lget("profile_banner_url", ""),
+ "profile_image" : user["avatar"]["image_url"].replace(
+ "_normal.", "."),
+ "favourites_count": lget("favourites_count"),
+ "followers_count" : lget("followers_count"),
+ "friends_count" : lget("friends_count"),
+ "listed_count" : lget("listed_count"),
+ "media_count" : lget("media_count"),
+ "statuses_count" : lget("statuses_count"),
}
- descr = user["description"]
+ descr = legacy["description"]
if urls := entities["description"].get("urls"):
for url in urls:
try:
@@ -604,34 +641,92 @@ class TwitterExtractor(Extractor):
return self.cookies_update(_login_impl(self, username, password))
+class TwitterHomeExtractor(TwitterExtractor):
+ """Extractor for Twitter home timelines"""
+ subcategory = "home"
+ pattern = (BASE_PATTERN +
+ r"/(?:home(?:/fo(?:llowing|r[-_ ]?you()))?|i/timeline)/?$")
+ example = "https://x.com/home"
+
+ def tweets(self):
+ if self.groups[0] is None:
+ return self.api.home_latest_timeline()
+ return self.api.home_timeline()
+
+
+class TwitterSearchExtractor(TwitterExtractor):
+ """Extractor for Twitter search results"""
+ subcategory = "search"
+ pattern = BASE_PATTERN + r"/search/?\?(?:[^&#]+&)*q=([^&#]+)"
+ example = "https://x.com/search?q=QUERY"
+
+ def metadata(self):
+ return {"search": text.unquote(self.user)}
+
+ def tweets(self):
+ query = text.unquote(self.user.replace("+", " "))
+
+ user = None
+ for item in query.split():
+ item = item.strip("()")
+ if item.startswith("from:"):
+ if user:
+ user = None
+ break
+ else:
+ user = item[5:]
+
+ if user is not None:
+ try:
+ self._assign_user(self.api.user_by_screen_name(user))
+ except KeyError:
+ pass
+
+ return self.api.search_timeline(query)
+
+
+class TwitterHashtagExtractor(TwitterExtractor):
+ """Extractor for Twitter hashtags"""
+ subcategory = "hashtag"
+ pattern = BASE_PATTERN + r"/hashtag/([^/?#]+)"
+ example = "https://x.com/hashtag/NAME"
+
+ def items(self):
+ url = f"{self.root}/search?q=%23{self.user}"
+ data = {"_extractor": TwitterSearchExtractor}
+ yield Message.Queue, url, data
+
+
class TwitterUserExtractor(Dispatch, TwitterExtractor):
"""Extractor for a Twitter user"""
- pattern = (BASE_PATTERN + r"/(?!search)(?:([^/?#]+)/?(?:$|[?#])"
+ pattern = (BASE_PATTERN + r"/(?:"
+ r"([^/?#]+)/?(?:$|\?|#)"
r"|i(?:/user/|ntent/user\?user_id=)(\d+))")
example = "https://x.com/USER"
def items(self):
user, user_id = self.groups
if user_id is not None:
- user = "id:" + user_id
+ user = f"id:{user_id}"
base = f"{self.root}/{user}/"
return self._dispatch_extractors((
- (TwitterInfoExtractor , base + "info"),
- (TwitterAvatarExtractor , base + "photo"),
- (TwitterBackgroundExtractor, base + "header_photo"),
- (TwitterTimelineExtractor , base + "timeline"),
- (TwitterTweetsExtractor , base + "tweets"),
- (TwitterMediaExtractor , base + "media"),
- (TwitterRepliesExtractor , base + "with_replies"),
- (TwitterLikesExtractor , base + "likes"),
+ (TwitterInfoExtractor , f"{base}info"),
+ (TwitterAvatarExtractor , f"{base}photo"),
+ (TwitterBackgroundExtractor, f"{base}header_photo"),
+ (TwitterTimelineExtractor , f"{base}timeline"),
+ (TwitterTweetsExtractor , f"{base}tweets"),
+ (TwitterMediaExtractor , f"{base}media"),
+ (TwitterRepliesExtractor , f"{base}with_replies"),
+ (TwitterHighlightsExtractor, f"{base}highlights"),
+ (TwitterLikesExtractor , f"{base}likes"),
), ("timeline",))
class TwitterTimelineExtractor(TwitterExtractor):
"""Extractor for a Twitter user timeline"""
subcategory = "timeline"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/timeline(?!\w)"
+ pattern = rf"{USER_PATTERN}/timeline(?!\w)"
example = "https://x.com/USER/timeline"
def _init_cursor(self):
@@ -728,7 +823,7 @@ class TwitterTimelineExtractor(TwitterExtractor):
class TwitterTweetsExtractor(TwitterExtractor):
"""Extractor for Tweets from a user's Tweets timeline"""
subcategory = "tweets"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/tweets(?!\w)"
+ pattern = rf"{USER_PATTERN}/tweets(?!\w)"
example = "https://x.com/USER/tweets"
def tweets(self):
@@ -738,17 +833,27 @@ class TwitterTweetsExtractor(TwitterExtractor):
class TwitterRepliesExtractor(TwitterExtractor):
"""Extractor for Tweets from a user's timeline including replies"""
subcategory = "replies"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/with_replies(?!\w)"
+ pattern = rf"{USER_PATTERN}/with_replies(?!\w)"
example = "https://x.com/USER/with_replies"
def tweets(self):
return self.api.user_tweets_and_replies(self.user)
+class TwitterHighlightsExtractor(TwitterExtractor):
+ """Extractor for Tweets from a user's highlights timeline"""
+ subcategory = "highlights"
+ pattern = rf"{USER_PATTERN}/highlights(?!\w)"
+ example = "https://x.com/USER/highlights"
+
+ def tweets(self):
+ return self.api.user_highlights(self.user)
+
+
class TwitterMediaExtractor(TwitterExtractor):
"""Extractor for Tweets from a user's Media timeline"""
subcategory = "media"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/media(?!\w)"
+ pattern = rf"{USER_PATTERN}/media(?!\w)"
example = "https://x.com/USER/media"
def tweets(self):
@@ -758,7 +863,7 @@ class TwitterMediaExtractor(TwitterExtractor):
class TwitterLikesExtractor(TwitterExtractor):
"""Extractor for liked tweets"""
subcategory = "likes"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/likes(?!\w)"
+ pattern = rf"{USER_PATTERN}/likes(?!\w)"
example = "https://x.com/USER/likes"
def metadata(self):
@@ -808,7 +913,7 @@ class TwitterListMembersExtractor(TwitterExtractor):
class TwitterFollowingExtractor(TwitterExtractor):
"""Extractor for followed users"""
subcategory = "following"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/following(?!\w)"
+ pattern = rf"{USER_PATTERN}/following(?!\w)"
example = "https://x.com/USER/following"
def items(self):
@@ -819,7 +924,7 @@ class TwitterFollowingExtractor(TwitterExtractor):
class TwitterFollowersExtractor(TwitterExtractor):
"""Extractor for a user's followers"""
subcategory = "followers"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/followers(?!\w)"
+ pattern = rf"{USER_PATTERN}/followers(?!\w)"
example = "https://x.com/USER/followers"
def items(self):
@@ -827,52 +932,12 @@ class TwitterFollowersExtractor(TwitterExtractor):
return self._users_result(TwitterAPI(self).user_followers(self.user))
-class TwitterSearchExtractor(TwitterExtractor):
- """Extractor for Twitter search results"""
- subcategory = "search"
- pattern = BASE_PATTERN + r"/search/?\?(?:[^&#]+&)*q=([^&#]+)"
- example = "https://x.com/search?q=QUERY"
-
- def metadata(self):
- return {"search": text.unquote(self.user)}
-
- def tweets(self):
- query = text.unquote(self.user.replace("+", " "))
-
- user = None
- for item in query.split():
- item = item.strip("()")
- if item.startswith("from:"):
- if user:
- user = None
- break
- else:
- user = item[5:]
-
- if user is not None:
- try:
- self._assign_user(self.api.user_by_screen_name(user))
- except KeyError:
- pass
-
- return self.api.search_timeline(query)
-
-
-class TwitterHashtagExtractor(TwitterExtractor):
- """Extractor for Twitter hashtags"""
- subcategory = "hashtag"
- pattern = BASE_PATTERN + r"/hashtag/([^/?#]+)"
- example = "https://x.com/hashtag/NAME"
-
- def items(self):
- url = f"{self.root}/search?q=%23{self.user}"
- data = {"_extractor": TwitterSearchExtractor}
- yield Message.Queue, url, data
-
-
class TwitterCommunityExtractor(TwitterExtractor):
"""Extractor for a Twitter community"""
subcategory = "community"
+ directory_fmt = ("{category}", "Communities",
+ "{community[name]} ({community[id]})")
+ archive_fmt = "C_{community[id]}_{tweet_id}_{num}"
pattern = BASE_PATTERN + r"/i/communities/(\d+)"
example = "https://x.com/i/communities/12345"
@@ -885,6 +950,8 @@ class TwitterCommunityExtractor(TwitterExtractor):
class TwitterCommunitiesExtractor(TwitterExtractor):
"""Extractor for followed Twitter communities"""
subcategory = "communities"
+ directory_fmt = TwitterCommunityExtractor.directory_fmt
+ archive_fmt = TwitterCommunityExtractor.archive_fmt
pattern = BASE_PATTERN + r"/([^/?#]+)/communities/?$"
example = "https://x.com/i/communities"
@@ -1002,7 +1069,7 @@ class TwitterQuotesExtractor(TwitterExtractor):
class TwitterInfoExtractor(TwitterExtractor):
"""Extractor for a user's profile data"""
subcategory = "info"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/info"
+ pattern = rf"{USER_PATTERN}/info"
example = "https://x.com/USER/info"
def items(self):
@@ -1021,13 +1088,13 @@ class TwitterAvatarExtractor(TwitterExtractor):
subcategory = "avatar"
filename_fmt = "avatar {date}.{extension}"
archive_fmt = "AV_{user[id]}_{date}"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/photo"
+ pattern = rf"{USER_PATTERN}/photo"
example = "https://x.com/USER/photo"
def tweets(self):
self.api._user_id_by_screen_name(self.user)
user = self._user_obj
- url = user["legacy"]["profile_image_url_https"]
+ url = user["avatar"]["image_url"]
if url == ("https://abs.twimg.com/sticky"
"/default_profile_images/default_profile_normal.png"):
@@ -1043,7 +1110,7 @@ class TwitterBackgroundExtractor(TwitterExtractor):
subcategory = "background"
filename_fmt = "background {date}.{extension}"
archive_fmt = "BG_{user[id]}_{date}"
- pattern = BASE_PATTERN + r"/(?!search)([^/?#]+)/header_photo"
+ pattern = rf"{USER_PATTERN}/header_photo"
example = "https://x.com/USER/header_photo"
def tweets(self):
@@ -1169,9 +1236,10 @@ class TwitterAPI():
}
self.features = {
"hidden_profile_subscriptions_enabled": True,
+ "payments_enabled": False,
+ "rweb_xchat_enabled": False,
"profile_label_improvements_pcf_label_in_post_enabled": True,
"rweb_tipjar_consumption_enabled": True,
- "responsive_web_graphql_exclude_directive_enabled": True,
"verified_phone_label_enabled": False,
"highlights_tweets_tab_ui_enabled": True,
"responsive_web_twitter_article_notes_tab_enabled": True,
@@ -1179,26 +1247,26 @@ class TwitterAPI():
"creator_subscriptions_tweet_preview_api_enabled": True,
"responsive_web_graphql_"
"skip_user_profile_image_extensions_enabled": False,
- "responsive_web_graphql_"
- "timeline_navigation_enabled": True,
+ "responsive_web_graphql_timeline_navigation_enabled": True,
}
self.features_pagination = {
"rweb_video_screen_enabled": False,
+ "payments_enabled": False,
+ "rweb_xchat_enabled": False,
"profile_label_improvements_pcf_label_in_post_enabled": True,
"rweb_tipjar_consumption_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,
+ "responsive_web_graphql"
+ "_timeline_navigation_enabled": True,
+ "responsive_web_graphql"
+ "_skip_user_profile_image_extensions_enabled": False,
"premium_content_api_read_enabled": False,
"communities_web_enable_tweet_community_results_fetch": True,
"c9s_tweet_anatomy_moderator_badge_enabled": True,
"responsive_web_grok_analyze_button_fetch_trends_enabled": False,
"responsive_web_grok_analyze_post_followups_enabled": True,
- "responsive_web_jetfuel_frame": False,
+ "responsive_web_jetfuel_frame": True,
"responsive_web_grok_share_attachment_enabled": True,
"articles_preview_enabled": True,
"responsive_web_edit_tweet_api_enabled": True,
@@ -1212,22 +1280,27 @@ class TwitterAPI():
"creator_subscriptions_quote_tweet_preview_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,
+ "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_grok_image_annotation_enabled": True,
+ "responsive_web_grok_imagine_annotation_enabled": True,
+ "responsive_web_grok"
+ "_community_note_auto_translation_is_enabled": False,
"responsive_web_enhance_cards_enabled": False,
}
def tweet_result_by_rest_id(self, tweet_id):
- endpoint = "/graphql/Vg2Akr5FzUmF0sTplA5k6g/TweetResultByRestId"
+ endpoint = "/graphql/qxWQxcMLiTPcavz9Qy5hwQ/TweetResultByRestId"
variables = {
"tweetId": tweet_id,
"withCommunity": False,
"includePromotedContent": False,
"withVoice": False,
}
+ features = self.features_pagination.copy()
+ del features["rweb_video_screen_enabled"]
field_toggles = {
"withArticleRichContentState": True,
"withArticlePlainText": False,
@@ -1236,7 +1309,7 @@ class TwitterAPI():
}
params = {
"variables" : self._json_dumps(variables),
- "features" : self._json_dumps(self.features_pagination),
+ "features" : self._json_dumps(features),
"fieldToggles": self._json_dumps(field_toggles),
}
tweet = self._call(endpoint, params)["data"]["tweetResult"]["result"]
@@ -1245,16 +1318,16 @@ class TwitterAPI():
if tweet.get("__typename") == "TweetUnavailable":
reason = tweet.get("reason")
- if reason == "NsfwLoggedOut":
- raise exception.AuthorizationError("NSFW Tweet")
+ if reason in ("NsfwViewerHasNoStatedAge", "NsfwLoggedOut"):
+ raise exception.AuthRequired(message="NSFW Tweet")
if reason == "Protected":
- raise exception.AuthorizationError("Protected Tweet")
+ raise exception.AuthRequired(message="Protected Tweet")
raise exception.AbortExtraction(f"Tweet unavailable ('{reason}')")
return tweet
def tweet_detail(self, tweet_id):
- endpoint = "/graphql/b9Yw90FMr_zUb8DvA8r2ug/TweetDetail"
+ endpoint = "/graphql/iFEr5AcP121Og4wx9Yqo3w/TweetDetail"
variables = {
"focalTweetId": tweet_id,
"referrer": "profile",
@@ -1278,7 +1351,7 @@ class TwitterAPI():
field_toggles=field_toggles)
def user_tweets(self, screen_name):
- endpoint = "/graphql/M3Hpkrb8pjWkEuGdLeXMOA/UserTweets"
+ endpoint = "/graphql/E8Wq-_jFSaU7hxVcuOPR9g/UserTweets"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1293,7 +1366,7 @@ class TwitterAPI():
endpoint, variables, field_toggles=field_toggles)
def user_tweets_and_replies(self, screen_name):
- endpoint = "/graphql/pz0IHaV_t7T4HJavqqqcIA/UserTweetsAndReplies"
+ endpoint = "/graphql/-O3QOHrVn1aOm_cF5wyTCQ/UserTweetsAndReplies"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1307,8 +1380,22 @@ class TwitterAPI():
return self._pagination_tweets(
endpoint, variables, field_toggles=field_toggles)
+ def user_highlights(self, screen_name):
+ endpoint = "/graphql/gmHw9geMTncZ7jeLLUUNOw/UserHighlightsTweets"
+ variables = {
+ "userId": self._user_id_by_screen_name(screen_name),
+ "count": 100,
+ "includePromotedContent": False,
+ "withVoice": True,
+ }
+ field_toggles = {
+ "withArticlePlainText": False,
+ }
+ return self._pagination_tweets(
+ endpoint, variables, field_toggles=field_toggles)
+
def user_media(self, screen_name):
- endpoint = "/graphql/8B9DqlaGvYyOvTCzzZWtNA/UserMedia"
+ endpoint = "/graphql/jCRhbOzdgOHp6u9H4g2tEg/UserMedia"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1324,7 +1411,7 @@ class TwitterAPI():
endpoint, variables, field_toggles=field_toggles)
def user_likes(self, screen_name):
- endpoint = "/graphql/uxjTlmrTI61zreSIV1urbw/Likes"
+ endpoint = "/graphql/TGEKkJG_meudeaFcqaxM-Q/Likes"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1340,7 +1427,7 @@ class TwitterAPI():
endpoint, variables, field_toggles=field_toggles)
def user_bookmarks(self):
- endpoint = "/graphql/ztCdjqsvvdL0dE8R5ME0hQ/Bookmarks"
+ endpoint = "/graphql/pLtjrO4ubNh996M_Cubwsg/Bookmarks"
variables = {
"count": 100,
"includePromotedContent": False,
@@ -1348,29 +1435,35 @@ class TwitterAPI():
return self._pagination_tweets(
endpoint, variables, ("bookmark_timeline_v2", "timeline"), False)
- def list_latest_tweets_timeline(self, list_id):
- endpoint = "/graphql/LSefrrxhpeX8HITbKfWz9g/ListLatestTweetsTimeline"
- variables = {
- "listId": list_id,
- "count": 100,
- }
- return self._pagination_tweets(
- endpoint, variables, ("list", "tweets_timeline", "timeline"))
-
def search_timeline(self, query, product="Latest"):
- endpoint = "/graphql/fL2MBiqXPk5pSrOS5ACLdA/SearchTimeline"
+ endpoint = "/graphql/4fpceYZ6-YQCx_JSl_Cn_A/SearchTimeline"
variables = {
"rawQuery": query,
"count": 100,
"querySource": "typed_query",
"product": product,
+ "withGrokTranslatedBio": False,
}
return self._pagination_tweets(
endpoint, variables,
("search_by_raw_query", "search_timeline", "timeline"))
+ def community_query(self, community_id):
+ endpoint = "/graphql/2W09l7nD7ZbxGQHXvfB22w/CommunityQuery"
+ params = {
+ "variables": self._json_dumps({
+ "communityId": community_id,
+ }),
+ "features": self._json_dumps({
+ "c9s_list_members_action_api_enabled": False,
+ "c9s_superc9s_indication_enabled": False,
+ }),
+ }
+ return (self._call(endpoint, params)
+ ["data"]["communityResults"]["result"])
+
def community_tweets_timeline(self, community_id):
- endpoint = "/graphql/awszcpgwaIeqqNfmzjxUow/CommunityTweetsTimeline"
+ endpoint = "/graphql/Nyt-88UX4-pPCImZNUl9RQ/CommunityTweetsTimeline"
variables = {
"communityId": community_id,
"count": 100,
@@ -1384,7 +1477,7 @@ class TwitterAPI():
"timeline"))
def community_media_timeline(self, community_id):
- endpoint = "/graphql/HfMuDHto2j3NKUeiLjKWHA/CommunityMediaTimeline"
+ endpoint = "/graphql/ZniZ7AAK_VVu1xtSx1V-gQ/CommunityMediaTimeline"
variables = {
"communityId": community_id,
"count": 100,
@@ -1396,7 +1489,7 @@ class TwitterAPI():
"timeline"))
def communities_main_page_timeline(self, screen_name):
- endpoint = ("/graphql/NbdrKPY_h_nlvZUg7oqH5Q"
+ endpoint = ("/graphql/p048a9n3hTPppQyK7FQTFw"
"/CommunitiesMainPageTimeline")
variables = {
"count": 100,
@@ -1406,6 +1499,27 @@ class TwitterAPI():
endpoint, variables,
("viewer", "communities_timeline", "timeline"))
+ def home_timeline(self):
+ endpoint = "/graphql/DXmgQYmIft1oLP6vMkJixw/HomeTimeline"
+ variables = {
+ "count": 100,
+ "includePromotedContent": False,
+ "latestControlAvailable": True,
+ "withCommunity": True,
+ }
+ return self._pagination_tweets(
+ endpoint, variables, ("home", "home_timeline_urt"))
+
+ def home_latest_timeline(self):
+ endpoint = "/graphql/SFxmNKWfN9ySJcXG_tjX8g/HomeLatestTimeline"
+ variables = {
+ "count": 100,
+ "includePromotedContent": False,
+ "latestControlAvailable": True,
+ }
+ return self._pagination_tweets(
+ endpoint, variables, ("home", "home_timeline_urt"))
+
def live_event_timeline(self, event_id):
endpoint = f"/2/live_event/timeline/{event_id}.json"
params = self.params.copy()
@@ -1422,8 +1536,17 @@ class TwitterAPI():
return (self._call(endpoint, params)
["twitter_objects"]["live_events"][event_id])
+ def list_latest_tweets_timeline(self, list_id):
+ endpoint = "/graphql/06JtmwM8k_1cthpFZITVVA/ListLatestTweetsTimeline"
+ variables = {
+ "listId": list_id,
+ "count": 100,
+ }
+ return self._pagination_tweets(
+ endpoint, variables, ("list", "tweets_timeline", "timeline"))
+
def list_members(self, list_id):
- endpoint = "/graphql/v97svwb-qcBmzv6QruDuNg/ListMembers"
+ endpoint = "/graphql/naea_MSad4pOb-D6_oVv_g/ListMembers"
variables = {
"listId": list_id,
"count": 100,
@@ -1432,35 +1555,38 @@ class TwitterAPI():
endpoint, variables, ("list", "members_timeline", "timeline"))
def user_followers(self, screen_name):
- endpoint = "/graphql/jqZ0_HJBA6mnu18iTZYm9w/Followers"
+ endpoint = "/graphql/i6PPdIMm1MO7CpAqjau7sw/Followers"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
"includePromotedContent": False,
+ "withGrokTranslatedBio": False,
}
return self._pagination_users(endpoint, variables)
def user_followers_verified(self, screen_name):
- endpoint = "/graphql/GHg0X_FjrJoISwwLPWi1LQ/BlueVerifiedFollowers"
+ endpoint = "/graphql/fxEl9kp1Tgolqkq8_Lo3sg/BlueVerifiedFollowers"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
"includePromotedContent": False,
+ "withGrokTranslatedBio": False,
}
return self._pagination_users(endpoint, variables)
def user_following(self, screen_name):
- endpoint = "/graphql/4QHbs4wmzgtU91f-t96_Eg/Following"
+ endpoint = "/graphql/SaWqzw0TFAWMx1nXWjXoaQ/Following"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
"includePromotedContent": False,
+ "withGrokTranslatedBio": False,
}
return self._pagination_users(endpoint, variables)
@memcache(keyarg=1)
def user_by_rest_id(self, rest_id):
- endpoint = "/graphql/5vdJ5sWkbSRDiiNZvwc2Yg/UserByRestId"
+ endpoint = "/graphql/8r5oa_2vD0WkhIAOkY4TTA/UserByRestId"
features = self.features
params = {
"variables": self._json_dumps({
@@ -1472,7 +1598,7 @@ class TwitterAPI():
@memcache(keyarg=1)
def user_by_screen_name(self, screen_name):
- endpoint = "/graphql/32pL5BWe9WKeSK1MoPvFQQ/UserByScreenName"
+ endpoint = "/graphql/ck5KkZ8t5cOmoLssopN99Q/UserByScreenName"
features = self.features.copy()
features["subscriptions_verification_info_"
"is_identity_verified_enabled"] = True
@@ -1481,6 +1607,7 @@ class TwitterAPI():
params = {
"variables": self._json_dumps({
"screen_name": screen_name,
+ "withGrokTranslatedBio": False,
}),
"features": self._json_dumps(features),
"fieldToggles": self._json_dumps({
@@ -1618,7 +1745,8 @@ class TwitterAPI():
return data
elif response.status_code in (403, 404) and \
not self.headers["x-twitter-auth-type"]:
- raise exception.AuthorizationError("Login required")
+ raise exception.AuthRequired(
+ "authenticated cookies", "timeline")
elif response.status_code == 429:
self._handle_ratelimit(response)
continue
@@ -1870,19 +1998,16 @@ class TwitterAPI():
continue
if "retweeted_status_result" in legacy:
- retweet = legacy["retweeted_status_result"]["result"]
- if "tweet" in retweet:
- retweet = retweet["tweet"]
- if original_retweets:
- try:
+ try:
+ retweet = legacy["retweeted_status_result"]["result"]
+ if "tweet" in retweet:
+ retweet = retweet["tweet"]
+ if original_retweets:
retweet["legacy"]["retweeted_status_id_str"] = \
retweet["rest_id"]
retweet["_retweet_id_str"] = tweet["rest_id"]
tweet = retweet
- except KeyError:
- continue
- else:
- try:
+ else:
legacy["retweeted_status_id_str"] = \
retweet["rest_id"]
tweet["author"] = \
@@ -1904,8 +2029,11 @@ class TwitterAPI():
rtlegacy["withheld_scope"]
legacy["full_text"] = rtlegacy["full_text"]
- except KeyError:
- pass
+ except Exception as exc:
+ extr.log.debug(
+ "%s: %s: %s",
+ tweet.get("rest_id"), exc.__class__.__name__, exc)
+ continue
yield tweet