From 124aea03bcd020f56f76052101d8602dcae5e550 Mon Sep 17 00:00:00 2001 From: d60 <94234663+d60@users.noreply.github.com> Date: Mon, 22 Apr 2024 19:53:16 +0900 Subject: [PATCH] Add files via upload --- twikit/client.py | 39 ++++++++++++++++++ twikit/tweet.py | 12 ++++++ twikit/twikit_async/client.py | 39 ++++++++++++++++++ twikit/twikit_async/tweet.py | 12 ++++++ twikit/utils.py | 77 ++++++++++++++++++++++++----------- 5 files changed, 155 insertions(+), 24 deletions(-) diff --git a/twikit/client.py b/twikit/client.py index bab4212d..d9c89470 100644 --- a/twikit/client.py +++ b/twikit/client.py @@ -38,6 +38,7 @@ TOKEN, USER_FEATURES, NOTE_TWEET_FEATURES, + SIMILAR_POSTS_FEATURES, Endpoint, Flow, Result, @@ -564,6 +565,44 @@ def search_user( next_cursor ) + def get_similar_tweets(self, tweet_id: str) -> list[Tweet]: + """ + Retrieves tweets similar to the specified tweet (Twitter premium only). + + Parameters + ---------- + tweet_id : :class:`str` + The ID of the tweet for which similar tweets are to be retrieved. + + Returns + ------- + list[:class:`Tweet`] + A list of Tweet objects representing tweets + similar to the specified tweet. + """ + params = flatten_params({ + 'variables': {'tweet_id': tweet_id}, + 'features': SIMILAR_POSTS_FEATURES + }) + response = self.http.get( + Endpoint.SIMILAR_POSTS, + params=params, + headers=self._base_headers + ).json() + + items = find_dict(response, 'entries')[0] + results = [] + for item in items: + if not item['entryId'].startswith('tweet'): + continue + tweet_data = find_dict(item, 'result')[0] + if 'tweet' in tweet_data: + tweet_data = tweet_data['tweet'] + user_data = tweet_data['core']['user_results']['result'] + results.append(Tweet(self, tweet_data, User(self, user_data))) + + return results + def upload_media( self, source: str | bytes, diff --git a/twikit/tweet.py b/twikit/tweet.py index d52c5c8a..bfb965b9 100644 --- a/twikit/tweet.py +++ b/twikit/tweet.py @@ -438,6 +438,18 @@ def get_favoriters( """ return self._client.get_favoriters(self.id, count, cursor) + def get_similar_tweets(self) -> list[Tweet]: + """ + Retrieves tweets similar to the tweet (Twitter premium only). + + Returns + ------- + list[:class:`Tweet`] + A list of Tweet objects representing tweets + similar to the tweet. + """ + return self._client.get_similar_tweets(self.id) + def update(self) -> None: new = self._client.get_tweet_by_id(self.id) self.__dict__.update(new.__dict__) diff --git a/twikit/twikit_async/client.py b/twikit/twikit_async/client.py index d5cc22cc..cbff6a16 100644 --- a/twikit/twikit_async/client.py +++ b/twikit/twikit_async/client.py @@ -28,6 +28,7 @@ LIST_FEATURES, TOKEN, USER_FEATURES, + SIMILAR_POSTS_FEATURES, NOTE_TWEET_FEATURES, Endpoint, build_tweet_data, @@ -569,6 +570,44 @@ async def search_user( next_cursor ) + async def get_similar_tweets(self, tweet_id: str) -> list[Tweet]: + """ + Retrieves tweets similar to the specified tweet (Twitter premium only). + + Parameters + ---------- + tweet_id : :class:`str` + The ID of the tweet for which similar tweets are to be retrieved. + + Returns + ------- + list[:class:`Tweet`] + A list of Tweet objects representing tweets + similar to the specified tweet. + """ + params = flatten_params({ + 'variables': {'tweet_id': tweet_id}, + 'features': SIMILAR_POSTS_FEATURES + }) + response = (await self.http.get( + Endpoint.SIMILAR_POSTS, + params=params, + headers=self._base_headers + )).json() + + items = find_dict(response, 'entries')[0] + results = [] + for item in items: + if not item['entryId'].startswith('tweet'): + continue + tweet_data = find_dict(item, 'result')[0] + if 'tweet' in tweet_data: + tweet_data = tweet_data['tweet'] + user_data = tweet_data['core']['user_results']['result'] + results.append(Tweet(self, tweet_data, User(self, user_data))) + + return results + async def upload_media( self, source: str | bytes, diff --git a/twikit/twikit_async/tweet.py b/twikit/twikit_async/tweet.py index 72c379e3..5ed871e7 100644 --- a/twikit/twikit_async/tweet.py +++ b/twikit/twikit_async/tweet.py @@ -435,6 +435,18 @@ async def get_favoriters( """ return await self._client.get_favoriters(self.id, count, cursor) + async def get_similar_tweets(self) -> list[Tweet]: + """ + Retrieves tweets similar to the tweet (Twitter premium only). + + Returns + ------- + list[:class:`Tweet`] + A list of Tweet objects representing tweets + similar to the tweet. + """ + return await self._client.get_similar_tweets(self.id) + async def update(self) -> None: new = await self._client.get_tweet_by_id(self.id) self.__dict__.update(new.__dict__) diff --git a/twikit/utils.py b/twikit/utils.py index 2ad8c45c..d1f3bcd8 100644 --- a/twikit/utils.py +++ b/twikit/utils.py @@ -110,30 +110,58 @@ } NOTE_TWEET_FEATURES = { - "communities_web_enable_tweet_community_results_fetch": True, - "c9s_tweet_anatomy_moderator_badge_enabled": True, - "tweetypie_unmention_optimization_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, - "creator_subscriptions_quote_tweet_preview_enabled": False, - "longform_notetweets_rich_text_read_enabled": True, - "longform_notetweets_inline_media_enabled": True, - "articles_preview_enabled": False, - "rweb_video_timestamps_enabled": True, - "rweb_tipjar_consumption_enabled": True, - "responsive_web_graphql_exclude_directive_enabled": True, - "verified_phone_label_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_media_interstitial_enabled": True, - "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False, - "responsive_web_graphql_timeline_navigation_enabled": True, - "responsive_web_enhance_cards_enabled": False + 'communities_web_enable_tweet_community_results_fetch': True, + 'c9s_tweet_anatomy_moderator_badge_enabled': True, + 'tweetypie_unmention_optimization_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, + 'creator_subscriptions_quote_tweet_preview_enabled': False, + 'longform_notetweets_rich_text_read_enabled': True, + 'longform_notetweets_inline_media_enabled': True, + 'articles_preview_enabled': False, + 'rweb_video_timestamps_enabled': True, + 'rweb_tipjar_consumption_enabled': True, + 'responsive_web_graphql_exclude_directive_enabled': True, + 'verified_phone_label_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_media_interstitial_enabled': True, + 'responsive_web_graphql_skip_user_profile_image_extensions_enabled': False, + 'responsive_web_graphql_timeline_navigation_enabled': True, + 'responsive_web_enhance_cards_enabled': False +} + +SIMILAR_POSTS_FEATURES = { + '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, + 'communities_web_enable_tweet_community_results_fetch': True, + 'c9s_tweet_anatomy_moderator_badge_enabled': True, + 'articles_preview_enabled': False, + 'tweetypie_unmention_optimization_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, + '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_media_interstitial_enabled': True, + 'rweb_video_timestamps_enabled': True, + 'longform_notetweets_rich_text_read_enabled': True, + 'longform_notetweets_inline_media_enabled': True, + 'responsive_web_enhance_cards_enabled': False } @@ -224,6 +252,7 @@ class Endpoint: COMMUNITY_MEMBERS = 'https://twitter.com/i/api/graphql/KDAssJ5lafCy-asH4wm1dw/membersSliceTimeline_Query' COMMUNITY_MODERATORS = 'https://twitter.com/i/api/graphql/9KI_r8e-tgp3--N5SZYVjg/moderatorsSliceTimeline_Query' SEARCH_COMMUNITY_TWEET = 'https://twitter.com/i/api/graphql/5341rmzzvdjqfmPKfoHUBw/CommunityTweetSearchModuleQuery' + SIMILAR_POSTS = 'https://twitter.com/i/api/graphql/EToazR74i0rJyZYalfVEAQ/SimilarPosts' T = TypeVar('T')