diff --git a/.gitignore b/.gitignore index 1a1e207b..5def3419 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.json /*.py -/node_modules \ No newline at end of file +/node_modules +__pycache__ \ No newline at end of file diff --git a/twikit/client/client.py b/twikit/client/client.py index 053f0087..26c0bd79 100644 --- a/twikit/client/client.py +++ b/twikit/client/client.py @@ -18,7 +18,7 @@ from .._captcha import Capsolver from ..bookmark import BookmarkFolder from ..community import Community, CommunityMember -from ..constants import TOKEN, DOMAIN +from ..constants import TOKEN, DOMAIN, TIMELINE_IDS from ..errors import ( AccountLocked, AccountSuspended, @@ -2594,30 +2594,30 @@ async def get_trends( ... """ category = category.lower() - if category in ['news', 'sports', 'entertainment']: - category += '_unified' - response, _ = await self.v11.guide(category, count, additional_request_params) - - entry_id_prefix = 'trends' if category == 'trending' else 'Guide' + timeline_id = TIMELINE_IDS.get(category) + if timeline_id is None: + return [] + + response, _ = await self.gql.generic_timeline_by_id(timeline_id, count) + entry_id_prefix = "trend" entries = [ i for i in find_dict(response, 'entries', find_one=True)[0] if i['entryId'].startswith(entry_id_prefix) ] - if not entries: - if not retry: - return [] - # Recall the method again, as the trend information - # may not be returned due to a Twitter error. - return await self.get_trends(category, count, retry, additional_request_params) - - items = entries[-1]['content']['timelineModule']['items'] - + if not retry: + return [] + # Recall the method again, as the trend information + # may not be returned due to a Twitter error. + return await self.get_trends(category, count, retry, additional_request_params) + results = [] - for item in items: - trend_info = item['item']['content']['trend'] + for entry in entries: + item_content = entry['content'].get('itemContent', {}) + trend_info = item_content.get('trend') + if not trend_info: + continue results.append(Trend(self, trend_info)) - return results async def get_available_locations(self) -> list[Location]: diff --git a/twikit/client/gql.py b/twikit/client/gql.py index 54d7d570..6ce24605 100644 --- a/twikit/client/gql.py +++ b/twikit/client/gql.py @@ -15,7 +15,9 @@ TWEET_RESULT_BY_REST_ID_FEATURES, TWEET_RESULTS_BY_REST_IDS_FEATURES, USER_FEATURES, - USER_HIGHLIGHTS_TWEETS_FEATURES + USER_HIGHLIGHTS_TWEETS_FEATURES, + EXPLORE_PAGE_FEATURES, + GENERIC_TIMELINE_FEATURES ) from ..utils import flatten_params, get_query_id @@ -68,6 +70,8 @@ def url(path): DELETE_BOOKMARK_FOLDER = url('2UTTsO-6zs93XqlEUZPsSg/DeleteBookmarkFolder') CREATE_BOOKMARK_FOLDER = url('6Xxqpq8TM_CREYiuof_h5w/createBookmarkFolder') FOLLOWERS = url('gC_lyAxZOptAMLCJX5UhWw/Followers') + EXPLORE_PAGE = url('Lr7rbLxwMLDrWFJrlCdRVw/ExplorePage') + GENERIC_TIMELINE_BY_ID = url('J5pGd3g_8gGG28OGzHci8g/GenericTimelineById') BLUE_VERIFIED_FOLLOWERS = url('VmIlPJNEDVQ29HfzIhV4mw/BlueVerifiedFollowers') FOLLOWERS_YOU_KNOW = url('f2tbuGNjfOE8mNUO5itMew/FollowersYouKnow') FOLLOWING = url('2vUj-_Ek-UmBVDNtd8OnQA/Following') @@ -301,6 +305,17 @@ async def retweeters(self, tweet_id, count, cursor): async def favoriters(self, tweet_id, count, cursor): return await self.tweet_engagements(tweet_id, count, cursor, Endpoint.FAVORITERS) + async def explore_page(self): + variables = {'cursor': ''} + return await self.gql_get(Endpoint.EXPLORE_PAGE, variables, EXPLORE_PAGE_FEATURES) + + async def generic_timeline_by_id(self, timeline_id, count): + variables = { + 'timelineId': timeline_id, + 'count': count + } + return await self.gql_get(Endpoint.GENERIC_TIMELINE_BY_ID, variables, GENERIC_TIMELINE_FEATURES) + async def bird_watch_one_note(self, note_id): variables = {'note_id': note_id} return await self.gql_get(Endpoint.FETCH_COMMUNITY_NOTE, variables, COMMUNITY_NOTE_FEATURES) diff --git a/twikit/constants.py b/twikit/constants.py index 3d7dbb28..24c8e42e 100644 --- a/twikit/constants.py +++ b/twikit/constants.py @@ -258,3 +258,86 @@ 'responsive_web_graphql_timeline_navigation_enabled': True, 'responsive_web_enhance_cards_enabled': False } + +EXPLORE_PAGE_FEATURES = { + "rweb_video_screen_enabled": False, + "payments_enabled": False, + "profile_label_improvements_pcf_label_in_post_enabled": True, + "rweb_tipjar_consumption_enabled": True, + "verified_phone_label_enabled": False, + "responsive_web_graphql_timeline_navigation_enabled": True, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False, + "creator_subscriptions_tweet_preview_api_enabled": True, + "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": True, + "responsive_web_grok_share_attachment_enabled": True, + "articles_preview_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": True, + "tweet_awards_web_tipping_enabled": False, + "responsive_web_grok_show_grok_translated_post": False, + "responsive_web_grok_analysis_button_from_backend": True, + "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, + "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 +} + +GENERIC_TIMELINE_FEATURES = { + "rweb_video_screen_enabled": False, + "payments_enabled": False, + "profile_label_improvements_pcf_label_in_post_enabled": True, + "rweb_tipjar_consumption_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, + "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": True, + "responsive_web_grok_share_attachment_enabled": True, + "articles_preview_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": True, + "tweet_awards_web_tipping_enabled": False, + "responsive_web_grok_show_grok_translated_post": False, + "responsive_web_grok_analysis_button_from_backend": True, + "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, + "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 +} + +# NOTE: you can fetch these using explore_page +TIMELINE_IDS = { + 'trending': 'VGltZWxpbmU6DAC2CwABAAAACHRyZW5kaW5nAAA=', + 'for-you': 'VGltZWxpbmU6DAC2CwABAAAAB2Zvcl95b3UAAA==', + 'news': 'VGltZWxpbmU6DAC2CwABAAAABG5ld3MAAA==', + 'sports': 'VGltZWxpbmU6DAC2CwABAAAABnNwb3J0cwAA', + 'entertainment': 'VGltZWxpbmU6DAC2CwABAAAADWVudGVydGFpbm1lbnQAAA==', +} \ No newline at end of file diff --git a/twikit/trend.py b/twikit/trend.py index 50b92999..2cc70b6e 100644 --- a/twikit/trend.py +++ b/twikit/trend.py @@ -23,12 +23,13 @@ class Trend: def __init__(self, client: Client, data: dict) -> None: self._client = client - metadata: dict = data['trendMetadata'] + metadata: dict = data['trend_metadata'] self.name: str = data['name'] - self.tweets_count: int | None = metadata.get('metaDescription') - self.domain_context: str = metadata.get('domainContext') + # TODO: parse to int + self.tweets_count: str | None = metadata.get('meta_description') + self.domain_context: str = metadata.get('domain_context') self.grouped_trends: list[str] = [ - trend['name'] for trend in data.get('groupedTrends', []) + trend['name'] for trend in data.get('grouped_trends', []) ] def __repr__(self) -> str: