From 66e2912083e9331f1ec46faf2d478e07c838be6d Mon Sep 17 00:00:00 2001 From: d60 <94234663+d60@users.noreply.github.com> Date: Wed, 24 Apr 2024 01:28:35 +0900 Subject: [PATCH] Add files via upload --- twikit/__init__.py | 3 +- twikit/client.py | 202 ++++++++++++++++++++++++++++--- twikit/errors.py | 3 + twikit/tweet.py | 6 +- twikit/twikit_async/__init__.py | 1 + twikit/twikit_async/client.py | 204 +++++++++++++++++++++++++++++--- twikit/twikit_async/tweet.py | 6 +- twikit/utils.py | 34 ++++++ 8 files changed, 417 insertions(+), 42 deletions(-) diff --git a/twikit/__init__.py b/twikit/__init__.py index 9161de33..1a12ba77 100644 --- a/twikit/__init__.py +++ b/twikit/__init__.py @@ -6,8 +6,9 @@ A Python library for interacting with the Twitter API. """ -__version__ = '1.5.5' +__version__ = '1.5.6' +from .bookmark import BookmarkFolder from .client import Client from .community import (Community, CommunityCreator, CommunityMember, CommunityRule) diff --git a/twikit/client.py b/twikit/client.py index aa69ff34..ad12034e 100644 --- a/twikit/client.py +++ b/twikit/client.py @@ -11,6 +11,7 @@ from fake_useragent import UserAgent from httpx import Response +from .bookmark import BookmarkFolder from .community import Community, CommunityMember from .errors import ( CouldNotTweet, @@ -30,6 +31,7 @@ from .tweet import CommunityNote, Poll, ScheduledTweet, Tweet from .user import User from .utils import ( + BOOKMARK_FOLDER_TIMELINE_FEATURES, COMMUNITY_TWEETS_FEATURES, COMMUNITY_NOTE_FEATURES, JOIN_COMMUNITY_FEATURES, @@ -1432,6 +1434,8 @@ def get_tweet_by_id( show_replies = None # Reply to reply for reply in entry['content']['items'][1:]: + if 'tweetcomposer' in reply['entryId']: + continue if 'tweet' in find_dict(reply, 'result'): reply = reply['tweet'] if 'tweet' in reply.get('entryId'): @@ -2134,7 +2138,9 @@ def delete_retweet(self, tweet_id: str) -> Response: ) return response - def bookmark_tweet(self, tweet_id: str) -> Response: + def bookmark_tweet( + self, tweet_id: str, folder_id: str | None = None + ) -> Response: """ Adds the tweet to bookmarks. @@ -2142,6 +2148,8 @@ def bookmark_tweet(self, tweet_id: str) -> Response: ---------- tweet_id : :class:`str` The ID of the tweet to be bookmarked. + folder_id : :class:`str` | None, default=None + The ID of the folder to add the bookmark to. Returns ------- @@ -2152,18 +2160,20 @@ def bookmark_tweet(self, tweet_id: str) -> Response: -------- >>> tweet_id = '...' >>> client.bookmark_tweet(tweet_id) - - See Also - -------- - .bookmark_tweet """ + variables = {'tweet_id': tweet_id} + if folder_id is None: + endpoint = Endpoint.CREATE_BOOKMARK + else: + endpoint = Endpoint.BOOKMARK_TO_FOLDER + variables['bookmark_collection_id'] = folder_id data = { - 'variables': {'tweet_id': tweet_id}, + 'variables': variables, 'queryId': get_query_id(Endpoint.CREATE_BOOKMARK) } response = self.http.post( - Endpoint.CREATE_BOOKMARK, + endpoint, json=data, headers=self._base_headers ) @@ -2204,7 +2214,8 @@ def delete_bookmark(self, tweet_id: str) -> Response: return response def get_bookmarks( - self, count: int = 20, cursor: str | None = None + self, count: int = 20, + cursor: str | None = None, folder_id: str | None = None ) -> Result[Tweet]: """ Retrieves bookmarks from the authenticated user's Twitter account. @@ -2212,9 +2223,9 @@ def get_bookmarks( Parameters ---------- count : :class:`int`, default=20 - The number of bookmarks to retrieve (default is 20). - cursor : :class:`str`, default=None - A cursor to paginate through the bookmarks (default is None). + The number of bookmarks to retrieve. + folder_id : :class:`str` | None, default=None + Folder to retrieve bookmarks. Returns ------- @@ -2240,17 +2251,24 @@ def get_bookmarks( 'count': count, 'includePromotedContent': True } + if folder_id is None: + endpoint = Endpoint.BOOKMARKS + features = FEATURES | { + 'graphql_timeline_v2_bookmark_timeline': True + } + else: + endpoint = Endpoint.BOOKMARK_FOLDER_TIMELINE + variables['bookmark_collection_id'] = folder_id + features = BOOKMARK_FOLDER_TIMELINE_FEATURES + if cursor is not None: variables['cursor'] = cursor - features = FEATURES | { - 'graphql_timeline_v2_bookmark_timeline': True - } params = flatten_params({ 'variables': variables, 'features': features }) response = self.http.get( - Endpoint.BOOKMARKS, + endpoint, params=params, headers=self._base_headers ).json() @@ -2260,7 +2278,13 @@ def get_bookmarks( return Result([]) items = items_[0] next_cursor = items[-1]['content']['value'] - previous_cursor = items[-2]['content']['value'] + if folder_id is None: + previous_cursor = items[-2]['content']['value'] + fetch_previous_result = partial(self.get_bookmarks, count, + previous_cursor, folder_id) + else: + previous_cursor = None + fetch_previous_result = None results = [] for item in items: @@ -2272,9 +2296,9 @@ def get_bookmarks( return Result( results, - partial(self.get_bookmarks, count, next_cursor), + partial(self.get_bookmarks, count, next_cursor, folder_id), next_cursor, - partial(self.get_bookmarks, count, previous_cursor), + fetch_previous_result, previous_cursor ) @@ -2302,6 +2326,148 @@ def delete_all_bookmarks(self) -> Response: ) return response + def get_bookmark_folders( + self, cursor: str | None = None + ) -> Result[BookmarkFolder]: + """ + Retrieves bookmark folders. + + Returns + ------- + Result[:class:`BookmarkFolder`] + Result object containing a list of bookmark folders. + + Examples + -------- + >>> folders = client.get_bookmark_folders() + >>> print(folders) + [, ..., ] + >>> more_folders = folders.next() # Retrieve more folders + """ + variables = {} + if cursor is not None: + variables['cursor'] = cursor + params = flatten_params({'variables': variables}) + response = self.http.get( + Endpoint.BOOKMARK_FOLDERS, + params=params, + headers=self._base_headers + ).json() + + slice = find_dict(response, 'bookmark_collections_slice')[0] + results = [] + for item in slice['items']: + results.append(BookmarkFolder(self, item)) + + if 'next_cursor' in slice['slice_info']: + next_cursor = slice['slice_info']['next_cursor'] + fetch_next_result = partial(self.get_bookmark_folders, next_cursor) + else: + next_cursor = None + fetch_next_result = None + + return Result( + results, + fetch_next_result, + next_cursor + ) + + def edit_bookmark_folder( + self, folder_id: str, name: str + ) -> BookmarkFolder: + """ + Edits a bookmark folder. + + Parameters + ---------- + folder_id : :class:`str` + ID of the folder to edit. + name : :class:`str` + New name for the folder. + + Returns + ------- + :class:`BookmarkFolder` + Updated bookmark folder. + + Examples + -------- + >>> client.edit_bookmark_folder('123456789', 'MyFolder') + """ + variables = { + 'bookmark_collection_id': folder_id, + 'name': name + } + data = { + 'variables': variables, + 'queryId': get_query_id(Endpoint.EDIT_BOOKMARK_FOLDER) + } + response = self.http.post( + Endpoint.EDIT_BOOKMARK_FOLDER, + json=data, + headers=self._base_headers + ).json() + return BookmarkFolder( + self, response['data']['bookmark_collection_update'] + ) + + def delete_bookmark_folder(self, folder_id: str) -> Response: + """ + Deletes a bookmark folder. + + Parameters + ---------- + folder_id : :class:`str` + ID of the folder to delete. + + Returns + ------- + :class:`httpx.Response` + Response returned from twitter api. + """ + variables = { + 'bookmark_collection_id': folder_id + } + data = { + 'variables': variables, + 'queryId': get_query_id(Endpoint.DELETE_BOOKMARK_FOLDER) + } + response = self.http.post( + Endpoint.DELETE_BOOKMARK_FOLDER, + json=data, + headers=self._base_headers + ) + return response + + def create_bookmark_folder(self, name: str) -> BookmarkFolder: + """Creates a bookmark folder. + + Parameters + ---------- + name : :class:`str` + Name of the folder. + + Returns + ------- + :class:`BookmarkFolder` + Newly created bookmark folder. + """ + variables = { + 'name': name + } + data = { + 'variables': variables, + 'queryId': get_query_id(Endpoint.CREATE_BOOKMARK_FOLDER) + } + response = self.http.post( + Endpoint.CREATE_BOOKMARK_FOLDER, + json=data, + headers=self._base_headers + ).json() + return BookmarkFolder( + self, response['data']['bookmark_collection_create'] + ) + def follow_user(self, user_id: str) -> Response: """ Follows a user. diff --git a/twikit/errors.py b/twikit/errors.py index 06bf0be7..076f869e 100644 --- a/twikit/errors.py +++ b/twikit/errors.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class TwitterException(Exception): """ Base class for Twitter API related exceptions. diff --git a/twikit/tweet.py b/twikit/tweet.py index e07e945f..08ba2c59 100644 --- a/twikit/tweet.py +++ b/twikit/tweet.py @@ -147,7 +147,8 @@ def __init__(self, client: Client, data: dict, user: User = None) -> None: self.reply_count: int = legacy['reply_count'] self.favorite_count: int = legacy['favorite_count'] self.favorited: bool = legacy['favorited'] - self.view_count: int = data['views'].get('count') if 'views' in data else None + self.view_count: int = (data['views'].get('count') + if 'views' in data else None) self.retweet_count: int = legacy['retweet_count'] self.editable_until_msecs: int = data['edit_control'].get( 'editable_until_msecs') @@ -155,7 +156,8 @@ def __init__(self, client: Client, data: dict, user: User = None) -> None: self.is_edit_eligible: bool = data['edit_control'].get( 'is_edit_eligible') self.edits_remaining: int = data['edit_control'].get('edits_remaining') - self.state: str = data['views'].get('state') if 'views' in data else None + self.state: str = (data['views'].get('state') + if 'views' in data else None) self.has_community_notes: bool = data.get('has_birdwatch_notes') if 'birdwatch_pivot' in data: diff --git a/twikit/twikit_async/__init__.py b/twikit/twikit_async/__init__.py index 8ebc4eee..dd0f0fa6 100644 --- a/twikit/twikit_async/__init__.py +++ b/twikit/twikit_async/__init__.py @@ -4,6 +4,7 @@ if os.name == 'nt': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) +from .bookmark import BookmarkFolder from ..errors import * from ..utils import build_query from .client import Client diff --git a/twikit/twikit_async/client.py b/twikit/twikit_async/client.py index d141ea81..d3fc0bef 100644 --- a/twikit/twikit_async/client.py +++ b/twikit/twikit_async/client.py @@ -21,6 +21,7 @@ raise_exceptions_from_response ) from ..utils import ( + BOOKMARK_FOLDER_TIMELINE_FEATURES, COMMUNITY_TWEETS_FEATURES, COMMUNITY_NOTE_FEATURES, FEATURES, @@ -38,6 +39,7 @@ get_query_id, urlencode ) +from .bookmark import BookmarkFolder from .community import ( Community, CommunityMember @@ -1449,6 +1451,8 @@ async def get_tweet_by_id( show_replies = None # Reply to reply for reply in entry['content']['items'][1:]: + if 'tweetcomposer' in reply['entryId']: + continue if 'tweet' in find_dict(reply, 'result'): reply = reply['tweet'] if 'tweet' in reply.get('entryId'): @@ -2155,7 +2159,9 @@ async def delete_retweet(self, tweet_id: str) -> Response: ) return response - async def bookmark_tweet(self, tweet_id: str) -> Response: + async def bookmark_tweet( + self, tweet_id: str, folder_id: str | None = None + ) -> Response: """ Adds the tweet to bookmarks. @@ -2163,6 +2169,8 @@ async def bookmark_tweet(self, tweet_id: str) -> Response: ---------- tweet_id : :class:`str` The ID of the tweet to be bookmarked. + folder_id : :class:`str` | None, default=None + The ID of the folder to add the bookmark to. Returns ------- @@ -2173,18 +2181,20 @@ async def bookmark_tweet(self, tweet_id: str) -> Response: -------- >>> tweet_id = '...' >>> await client.bookmark_tweet(tweet_id) - - See Also - -------- - .bookmark_tweet """ + variables = {'tweet_id': tweet_id} + if folder_id is None: + endpoint = Endpoint.CREATE_BOOKMARK + else: + endpoint = Endpoint.BOOKMARK_TO_FOLDER + variables['bookmark_collection_id'] = folder_id data = { - 'variables': {'tweet_id': tweet_id}, + 'variables': variables, 'queryId': get_query_id(Endpoint.CREATE_BOOKMARK) } response = await self.http.post( - Endpoint.CREATE_BOOKMARK, + endpoint, json=data, headers=self._base_headers ) @@ -2225,7 +2235,8 @@ async def delete_bookmark(self, tweet_id: str) -> Response: return response async def get_bookmarks( - self, count: int = 20, cursor: str | None = None + self, count: int = 20, + cursor: str | None = None, folder_id: str | None = None ) -> Result[Tweet]: """ Retrieves bookmarks from the authenticated user's Twitter account. @@ -2233,9 +2244,9 @@ async def get_bookmarks( Parameters ---------- count : :class:`int`, default=20 - The number of bookmarks to retrieve (default is 20). - cursor : :class:`str`, default=None - A cursor to paginate through the bookmarks (default is None). + The number of bookmarks to retrieve. + folder_id : :class:`str` | None, default=None + Folder to retrieve bookmarks. Returns ------- @@ -2251,7 +2262,7 @@ async def get_bookmarks( - >>> # To retrieve more bookmarks + >>> # # To retrieve more bookmarks >>> more_bookmarks = await bookmarks.next() >>> for bookmark in more_bookmarks: ... print(bookmark) @@ -2262,17 +2273,24 @@ async def get_bookmarks( 'count': count, 'includePromotedContent': True } + if folder_id is None: + endpoint = Endpoint.BOOKMARKS + features = FEATURES | { + 'graphql_timeline_v2_bookmark_timeline': True + } + else: + endpoint = Endpoint.BOOKMARK_FOLDER_TIMELINE + variables['bookmark_collection_id'] = folder_id + features = BOOKMARK_FOLDER_TIMELINE_FEATURES + if cursor is not None: variables['cursor'] = cursor - features = FEATURES | { - 'graphql_timeline_v2_bookmark_timeline': True - } params = flatten_params({ 'variables': variables, 'features': features }) response = (await self.http.get( - Endpoint.BOOKMARKS, + endpoint, params=params, headers=self._base_headers )).json() @@ -2282,7 +2300,13 @@ async def get_bookmarks( return Result([]) items = items_[0] next_cursor = items[-1]['content']['value'] - previous_cursor = items[-2]['content']['value'] + if folder_id is None: + previous_cursor = items[-2]['content']['value'] + fetch_previous_result = partial(self.get_bookmarks, count, + previous_cursor, folder_id) + else: + previous_cursor = None + fetch_previous_result = None results = [] for item in items: @@ -2294,9 +2318,9 @@ async def get_bookmarks( return Result( results, - partial(self.get_bookmarks, count, next_cursor), + partial(self.get_bookmarks, count, next_cursor, folder_id), next_cursor, - partial(self.get_bookmarks, count, previous_cursor), + fetch_previous_result, previous_cursor ) @@ -2324,6 +2348,148 @@ async def delete_all_bookmarks(self) -> Response: ) return response + async def get_bookmark_folders( + self, cursor: str | None = None + ) -> Result[BookmarkFolder]: + """ + Retrieves bookmark folders. + + Returns + ------- + Result[:class:`BookmarkFolder`] + Result object containing a list of bookmark folders. + + Examples + -------- + >>> folders = await client.get_bookmark_folders() + >>> print(folders) + [, ..., ] + >>> more_folders = await folders.next() # Retrieve more folders + """ + variables = {} + if cursor is not None: + variables['cursor'] = cursor + params = flatten_params({'variables': variables}) + response = (await self.http.get( + Endpoint.BOOKMARK_FOLDERS, + params=params, + headers=self._base_headers + )).json() + + slice = find_dict(response, 'bookmark_collections_slice')[0] + results = [] + for item in slice['items']: + results.append(BookmarkFolder(self, item)) + + if 'next_cursor' in slice['slice_info']: + next_cursor = slice['slice_info']['next_cursor'] + fetch_next_result = partial(self.get_bookmark_folders, next_cursor) + else: + next_cursor = None + fetch_next_result = None + + return Result( + results, + fetch_next_result, + next_cursor + ) + + async def edit_bookmark_folder( + self, folder_id: str, name: str + ) -> BookmarkFolder: + """ + Edits a bookmark folder. + + Parameters + ---------- + folder_id : :class:`str` + ID of the folder to edit. + name : :class:`str` + New name for the folder. + + Returns + ------- + :class:`BookmarkFolder` + Updated bookmark folder. + + Examples + -------- + >>> await client.edit_bookmark_folder('123456789', 'MyFolder') + """ + variables = { + 'bookmark_collection_id': folder_id, + 'name': name + } + data = { + 'variables': variables, + 'queryId': get_query_id(Endpoint.EDIT_BOOKMARK_FOLDER) + } + response = (await self.http.post( + Endpoint.EDIT_BOOKMARK_FOLDER, + json=data, + headers=self._base_headers + )).json() + return BookmarkFolder( + self, response['data']['bookmark_collection_update'] + ) + + async def delete_bookmark_folder(self, folder_id: str) -> Response: + """ + Deletes a bookmark folder. + + Parameters + ---------- + folder_id : :class:`str` + ID of the folder to delete. + + Returns + ------- + :class:`httpx.Response` + Response returned from twitter api. + """ + variables = { + 'bookmark_collection_id': folder_id + } + data = { + 'variables': variables, + 'queryId': get_query_id(Endpoint.DELETE_BOOKMARK_FOLDER) + } + response = await self.http.post( + Endpoint.DELETE_BOOKMARK_FOLDER, + json=data, + headers=self._base_headers + ) + return response + + async def create_bookmark_folder(self, name: str) -> BookmarkFolder: + """Creates a bookmark folder. + + Parameters + ---------- + name : :class:`str` + Name of the folder. + + Returns + ------- + :class:`BookmarkFolder` + Newly created bookmark folder. + """ + variables = { + 'name': name + } + data = { + 'variables': variables, + 'queryId': get_query_id(Endpoint.CREATE_BOOKMARK_FOLDER) + } + response = (await self.http.post( + Endpoint.CREATE_BOOKMARK_FOLDER, + json=data, + headers=self._base_headers + )).json() + return BookmarkFolder( + self, response['data']['bookmark_collection_create'] + ) + async def follow_user(self, user_id: str) -> Response: """ Follows a user. diff --git a/twikit/twikit_async/tweet.py b/twikit/twikit_async/tweet.py index ac465962..cc3e2ff9 100644 --- a/twikit/twikit_async/tweet.py +++ b/twikit/twikit_async/tweet.py @@ -144,7 +144,8 @@ def __init__(self, client: Client, data: dict, user: User = None) -> None: self.reply_count: int = legacy['reply_count'] self.favorite_count: int = legacy['favorite_count'] self.favorited: bool = legacy['favorited'] - self.view_count: int = data['views'].get('count') if 'views' in data else None + self.view_count: int = (data['views'].get('count') + if 'views' in data else None) self.retweet_count: int = legacy['retweet_count'] self.editable_until_msecs: int = data['edit_control'].get( 'editable_until_msecs') @@ -152,7 +153,8 @@ def __init__(self, client: Client, data: dict, user: User = None) -> None: self.is_edit_eligible: bool = data['edit_control'].get( 'is_edit_eligible') self.edits_remaining: int = data['edit_control'].get('edits_remaining') - self.state: str = data['views'].get('state') if 'views' in data else None + self.state: str = (data['views'].get('state') + if 'views' in data else None) self.has_community_notes: bool = data.get('has_birdwatch_notes') if 'birdwatch_pivot' in data: diff --git a/twikit/utils.py b/twikit/utils.py index d1f3bcd8..d885069f 100644 --- a/twikit/utils.py +++ b/twikit/utils.py @@ -164,6 +164,34 @@ 'responsive_web_enhance_cards_enabled': False } +BOOKMARK_FOLDER_TIMELINE_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 +} + class Endpoint: """ @@ -253,6 +281,12 @@ class Endpoint: 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' + BOOKMARK_FOLDERS = 'https://twitter.com/i/api/graphql/i78YDd0Tza-dV4SYs58kRg/BookmarkFoldersSlice' + EDIT_BOOKMARK_FOLDER = 'https://twitter.com/i/api/graphql/a6kPp1cS1Dgbsjhapz1PNw/EditBookmarkFolder' + DELETE_BOOKMARK_FOLDER = 'https://twitter.com/i/api/graphql/2UTTsO-6zs93XqlEUZPsSg/DeleteBookmarkFolder' + CREATE_BOOKMARK_FOLDER = 'https://twitter.com/i/api/graphql/6Xxqpq8TM_CREYiuof_h5w/createBookmarkFolder' + BOOKMARK_FOLDER_TIMELINE = 'https://twitter.com/i/api/graphql/8HoabOvl7jl9IC1Aixj-vg/BookmarkFolderTimeline' + BOOKMARK_TO_FOLDER = 'https://twitter.com/i/api/graphql/4KHZvvNbHNf07bsgnL9gWA/bookmarkTweetToFolder' T = TypeVar('T')