From 6418e7d579be1093257ff1f42212ec34dd798543 Mon Sep 17 00:00:00 2001 From: d60 Date: Wed, 29 Jan 2025 23:57:38 +0900 Subject: [PATCH] - Implemented transaction ID in guest client requests and fixed 404 error - Removed the dependency on requests - Fixed a bug in login with TOTP - Added Tweet.bookmark_count and Tweet.bookmarked" --- .gitignore | 1 + examples/listen_for_new_tweets.py | 2 +- twikit/__init__.py | 2 +- twikit/client/client.py | 24 ++++++++++++------------ twikit/guest/client.py | 28 +++++++++++++++++++++++----- twikit/guest/tweet.py | 6 ++++++ twikit/tweet.py | 6 ++++++ 7 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..94a2dd14 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/examples/listen_for_new_tweets.py b/examples/listen_for_new_tweets.py index ec2a6619..d81cc672 100644 --- a/examples/listen_for_new_tweets.py +++ b/examples/listen_for_new_tweets.py @@ -18,7 +18,7 @@ def callback(tweet: Tweet) -> None: async def get_latest_tweet() -> Tweet: - return await client.get_user_tweets(USER_ID, 'Replies')[0] + return (await client.get_user_tweets(USER_ID, 'Replies'))[0] async def main() -> NoReturn: diff --git a/twikit/__init__.py b/twikit/__init__.py index 082bc822..ec188aab 100644 --- a/twikit/__init__.py +++ b/twikit/__init__.py @@ -7,7 +7,7 @@ A Python library for interacting with the Twitter API. """ -__version__ = '2.2.1' +__version__ = '2.2.2' import asyncio import os diff --git a/twikit/client/client.py b/twikit/client/client.py index c81658d1..2eed1c83 100644 --- a/twikit/client/client.py +++ b/twikit/client/client.py @@ -436,18 +436,6 @@ async def login( }) return flow.response - await flow.execute_task({ - 'subtask_id': 'AccountDuplicationCheck', - 'check_logged_in_account': { - 'link': 'AccountDuplicationCheck_false' - } - }) - - if not flow.response['subtasks']: - return - - self._user_id = find_dict(flow.response, 'id_str', find_one=True)[0] - if flow.task_id == 'LoginTwoFactorAuthChallenge': if totp_secret is None: print(find_dict(flow.response, 'secondary_text', find_one=True)[0]['text']) @@ -463,6 +451,18 @@ async def login( } }) + await flow.execute_task({ + 'subtask_id': 'AccountDuplicationCheck', + 'check_logged_in_account': { + 'link': 'AccountDuplicationCheck_false' + } + }) + + if not flow.response['subtasks']: + return + + self._user_id = find_dict(flow.response, 'id_str', find_one=True)[0] + return flow.response async def logout(self) -> Response: diff --git a/twikit/guest/client.py b/twikit/guest/client.py index e5ddd7a9..17ada88a 100644 --- a/twikit/guest/client.py +++ b/twikit/guest/client.py @@ -4,13 +4,14 @@ import warnings from functools import partial from typing import Any, Literal +from urllib.parse import urlparse from httpx import AsyncClient, AsyncHTTPTransport, Response from httpx._utils import URLPattern from ..client.gql import GQLClient from ..client.v11 import V11Client -from ..constants import TOKEN +from ..constants import DOMAIN, TOKEN from ..errors import ( BadRequest, Forbidden, @@ -22,6 +23,7 @@ Unauthorized ) from ..utils import Result, find_dict, find_entry_by_type, httpx_transport_to_url +from ..x_client_transaction import ClientTransaction from .tweet import Tweet from .user import User @@ -48,7 +50,6 @@ def tweet_from_data(client: GuestClient, data: dict) -> Tweet: return Tweet(client, tweet_data, User(client, user_data)) - class GuestClient: """ A client for interacting with the Twitter API as a guest. @@ -71,7 +72,7 @@ class GuestClient: def __init__( self, - language: str | None = None, + language: str = 'en-US', proxy: str | None = None, **kwargs ) -> None: @@ -93,6 +94,7 @@ def __init__( self._guest_token: str | None = None # set when activate method is called self.gql = GQLClient(self) self.v11 = V11Client(self) + self.client_transaction = ClientTransaction() async def request( self, @@ -102,7 +104,23 @@ async def request( **kwargs ) -> tuple[dict | Any, Response]: ':meta private:' - response = await self.http.request(method, url, **kwargs) + headers = kwargs.pop('headers', {}) + + if not self.client_transaction.home_page_response: + cookies_backup = dict(self.http.cookies).copy() + ct_headers = { + 'Accept-Language': f'{self.language},{self.language.split("-")[0]};q=0.9', + 'Cache-Control': 'no-cache', + 'Referer': f'https://{DOMAIN}', + 'User-Agent': self._user_agent + } + await self.client_transaction.init(self.http, ct_headers) + self.http.cookies = cookies_backup + + tid = self.client_transaction.generate_transaction_id(method=method, path=urlparse(url).path) + headers['X-Client-Transaction-Id'] = tid + + response = await self.http.request(method, url, headers=headers, **kwargs) try: response_data = response.json() @@ -167,7 +185,7 @@ def _base_headers(self) -> dict[str, str]: 'authorization': f'Bearer {self._token}', 'content-type': 'application/json', 'X-Twitter-Active-User': 'yes', - 'Referer': 'https://twitter.com/', + 'Referer': f'https://{DOMAIN}', } if self.language is not None: diff --git a/twikit/guest/tweet.py b/twikit/guest/tweet.py index aaa41bf4..29caa039 100644 --- a/twikit/guest/tweet.py +++ b/twikit/guest/tweet.py @@ -53,6 +53,10 @@ class Tweet: The state of the tweet views. retweet_count : :class:`int` The count of retweets for the tweet. + bookmark_count : :class:`int` + The count of bookmarks for the tweet. + bookmarked : :class:`bool` + Indicates if the tweet is bookmarked. place : :class:`.Place` | None The location associated with the tweet. editable_until_msecs : :class:`int` @@ -107,6 +111,8 @@ def __init__(self, client: GuestClient, data: dict, user: User = None) -> None: self.favorited: bool = legacy['favorited'] self.retweet_count: int = legacy['retweet_count'] self._place_data = legacy.get('place') + self.bookmark_count: int = legacy.get('bookmark_count') + self.bookmarked: bool = legacy.get('bookmarked') self.editable_until_msecs: int = data['edit_control'].get('editable_until_msecs') self.is_translatable: bool = data.get('is_translatable') self.is_edit_eligible: bool = data['edit_control'].get('is_edit_eligible') diff --git a/twikit/tweet.py b/twikit/tweet.py index 55a271b0..1af36bcb 100644 --- a/twikit/tweet.py +++ b/twikit/tweet.py @@ -59,6 +59,10 @@ class Tweet: The state of the tweet views. retweet_count : :class:`int` The count of retweets for the tweet. + bookmark_count : :class:`int` + The count of bookmarks for the tweet. + bookmarked : :class:`bool` + Indicates if the tweet is bookmarked. place : :class:`.Place` | None The location associated with the tweet. editable_until_msecs : :class:`int` @@ -116,6 +120,8 @@ def __init__(self, client: Client, data: dict, user: User = None) -> None: self.favorited: bool = legacy['favorited'] self.retweet_count: int = legacy['retweet_count'] self._place_data = legacy.get('place') + self.bookmark_count: int = legacy.get('bookmark_count') + self.bookmarked: bool = legacy.get('bookmarked') self.editable_until_msecs: int = data['edit_control'].get('editable_until_msecs') self.is_translatable: bool = data.get('is_translatable') self.is_edit_eligible: bool = data['edit_control'].get('is_edit_eligible')