Skip to content

Commit

Permalink
v2.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
d60 committed Jul 25, 2024
1 parent a6d96be commit 5cdeb53
Show file tree
Hide file tree
Showing 13 changed files with 1,074 additions and 30 deletions.
26 changes: 26 additions & 0 deletions examples/guest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import asyncio

from twikit.guest import GuestClient

client = GuestClient()


async def main():
# Activate the client by generating a guest token.
await client.activate()

# Get user by screen name
user = await client.get_user_by_screen_name('elonmusk')
print(user)
# Get user by ID
user = await client.get_user_by_id('44196397')
print(user)


user_tweets = await client.get_user_tweets('44196397')
print(user_tweets)

tweet = await client.get_tweet_by_id('1519480761749016577')
print(tweet)

asyncio.run(main())
2 changes: 1 addition & 1 deletion twikit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
A Python library for interacting with the Twitter API.
"""

__version__ = '2.0.3'
__version__ = '2.1.0'

import asyncio
import os
Expand Down
69 changes: 68 additions & 1 deletion twikit/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from ..trend import Location, PlaceTrend, PlaceTrends, Trend
from ..tweet import CommunityNote, Poll, ScheduledTweet, Tweet, tweet_from_data
from ..user import User
from ..utils import Flow, Result, build_tweet_data, build_user_data, find_dict, httpx_transport_to_url
from ..utils import Flow, Result, build_tweet_data, build_user_data, find_dict, find_entry_by_type, httpx_transport_to_url
from .gql import GQLClient
from .v11 import V11Client

Expand Down Expand Up @@ -751,6 +751,73 @@ async def get_similar_tweets(self, tweet_id: str) -> list[Tweet]:

return results

async def get_user_highlights_tweets(
self,
user_id: str,
count: int = 20,
cursor: str | None = None
) -> Result[Tweet]:
"""
Retrieves highlighted tweets from a user's timeline.
Parameters
----------
user_id : :class:`str`
The user ID
count : :class:`int`, default=20
The number of tweets to retrieve.
Returns
-------
Result[:class:`Tweet`]
An instance of the `Result` class containing the highlighted tweets.
Examples
--------
>>> result = await client.get_user_highlights_tweets('123456789')
>>> for tweet in result:
... print(tweet)
<Tweet id="...">
<Tweet id="...">
...
...
>>> more_results = await result.next() # Retrieve more highlighted tweets
>>> for tweet in more_results:
... print(tweet)
<Tweet id="...">
<Tweet id="...">
...
...
"""
response, _ = await self.gql.user_highlights_tweets(user_id, count, cursor)

instructions = response['data']['user']['result']['timeline']['timeline']['instructions']
instruction = find_entry_by_type(instructions, 'TimelineAddEntries')
if instruction is None:
return Result.empty()
entries = instruction['entries']
previous_cursor = None
next_cursor = None
results = []

for entry in entries:
entryId = entry['entryId']
if entryId.startswith('tweet'):
results.append(tweet_from_data(self, entry))
elif entryId.startswith('cursor-top'):
previous_cursor = entry['content']['value']
elif entryId.startswith('cursor-bottom'):
next_cursor = entry['content']['value']

return Result(
results,
partial(self.get_user_highlights_tweets, user_id, count, next_cursor),
next_cursor,
partial(self.get_user_highlights_tweets, user_id, count, previous_cursor),
previous_cursor
)

async def upload_media(
self,
source: str | bytes,
Expand Down
91 changes: 68 additions & 23 deletions twikit/client/gql.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@
LIST_FEATURES,
NOTE_TWEET_FEATURES,
SIMILAR_POSTS_FEATURES,
USER_FEATURES
TWEET_RESULT_BY_REST_ID_FEATURES,
USER_FEATURES,
USER_HIGHLIGHTS_TWEETS_FEATURES
)
from ..utils import flatten_params, get_query_id

if TYPE_CHECKING:
from ..guest.client import GuestClient
from .client import Client

ClientType = Client | GuestClient


class Endpoint:
@staticmethod
Expand All @@ -33,6 +38,7 @@ def url(path):
USER_BY_SCREEN_NAME = url('NimuplG1OB7Fd2btCLdBOw/UserByScreenName')
USER_BY_REST_ID = url('tD8zKvQzwY3kdx5yz6YmOw/UserByRestId')
TWEET_DETAIL = url('U0HTv-bAWTBYylwEMT7x5A/TweetDetail')
TWEET_RESULT_BY_REST_ID = url('Xl5pC_lBk_gcO2ItU39DQw/TweetResultByRestId')
FETCH_SCHEDULED_TWEETS = url('ITtjAzvlZni2wWXwf295Qg/FetchScheduledTweets')
DELETE_SCHEDULED_TWEET = url('CTOVqej0JBXAZSwkp1US0g/DeleteScheduledTweet')
RETWEETERS = url('X-XEqG5qHQSAwmvy00xfyQ/Retweeters')
Expand All @@ -42,6 +48,7 @@ def url(path):
USER_TWEETS_AND_REPLIES = url('vMkJyzx1wdmvOeeNG0n6Wg/UserTweetsAndReplies')
USER_MEDIA = url('2tLOJWwGuCTytDrGBg8VwQ/UserMedia')
USER_LIKES = url('IohM3gxQHfvWePH5E3KuNA/Likes')
USER_HIGHLIGHTS_TWEETS = url('tHFm_XZc_NNi-CfUThwbNw/UserHighlightsTweets')
HOME_TIMELINE = url('-X_hcgQzmHGl29-UXxz4sw/HomeTimeline')
HOME_LATEST_TIMELINE = url('U0cdisy7QFIoTfu3-Okw0A/HomeLatestTimeline')
FAVORITE_TWEET = url('lI07N6Otwv1PhnEgXILM7A/FavoriteTweet')
Expand Down Expand Up @@ -92,7 +99,7 @@ def url(path):


class GQLClient:
def __init__(self, base: Client) -> None:
def __init__(self, base: ClientType) -> None:
self.base = base

async def gql_get(
Expand Down Expand Up @@ -320,27 +327,43 @@ async def user_media(self, user_id, count, cursor):
async def user_likes(self, user_id, count, cursor):
return await self._get_user_tweets(user_id, count, cursor, Endpoint.USER_LIKES)

async def user_highlights_tweets(self, user_id, count, cursor):
variables = {
'userId': user_id,
'count': count,
'includePromotedContent': True,
'withVoice': True
}
if cursor is not None:
variables['cursor'] = cursor
return await self.gql_get(
Endpoint.USER_HIGHLIGHTS_TWEETS,
variables,
USER_HIGHLIGHTS_TWEETS_FEATURES,
self.base._base_headers
)

async def home_timeline(self, count, seen_tweet_ids, cursor):
variables = {
"count": count,
"includePromotedContent": True,
"latestControlAvailable": True,
"requestContext": "launch",
"withCommunity": True,
"seenTweetIds": seen_tweet_ids or []
'count': count,
'includePromotedContent': True,
'latestControlAvailable': True,
'requestContext': 'launch',
'withCommunity': True,
'seenTweetIds': seen_tweet_ids or []
}
if cursor is not None:
variables['cursor'] = cursor
return await self.gql_post(Endpoint.HOME_TIMELINE, variables, FEATURES)

async def home_latest_timeline(self, count, seen_tweet_ids, cursor):
variables = {
"count": count,
"includePromotedContent": True,
"latestControlAvailable": True,
"requestContext": "launch",
"withCommunity": True,
"seenTweetIds": seen_tweet_ids or []
'count': count,
'includePromotedContent': True,
'latestControlAvailable': True,
'requestContext': 'launch',
'withCommunity': True,
'seenTweetIds': seen_tweet_ids or []
}
if cursor is not None:
variables['cursor'] = cursor
Expand Down Expand Up @@ -632,16 +655,38 @@ async def moderators_slice_timeline_query(self, community_id, count, cursor):

async def community_tweet_search_module_query(self, community_id, query, count, cursor):
variables = {
"count": count,
"query": query,
"communityId": community_id,
"includePromotedContent": False,
"withBirdwatchNotes": True,
"withVoice": False,
"isListMemberTargetUserId": "0",
"withCommunity": False,
"withSafetyModeUserFields": True
'count': count,
'query': query,
'communityId': community_id,
'includePromotedContent': False,
'withBirdwatchNotes': True,
'withVoice': False,
'isListMemberTargetUserId': '0',
'withCommunity': False,
'withSafetyModeUserFields': True
}
if cursor is not None:
variables['cursor'] = cursor
return await self.gql_get(Endpoint.COMMUNITY_TWEET_SEARCH_MODULE_QUERY, variables, COMMUNITY_TWEETS_FEATURES)

####################
# For guest client
####################

async def tweet_result_by_rest_id(self, tweet_id):
variables = {
'tweetId': tweet_id,
'withCommunity': False,
'includePromotedContent': False,
'withVoice': False
}
params = {
'fieldToggles': {
'withArticleRichContentState': True,
'withArticlePlainText': False,
'withGrokAnalyze': False
}
}
return await self.gql_get(
Endpoint.TWEET_RESULT_BY_REST_ID, variables, TWEET_RESULT_BY_REST_ID_FEATURES, extra_params=params
)
9 changes: 6 additions & 3 deletions twikit/client/v11.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ..guest.client import GuestClient
from .client import Client

ClientType = Client | GuestClient


class Endpoint:
GUEST_ACTIVATE = 'https://api.twitter.com/1.1/guest/activate.json'
Expand Down Expand Up @@ -45,13 +48,13 @@ class Endpoint:


class V11Client:
def __init__(self, base: Client) -> None:
def __init__(self, base: ClientType) -> None:
self.base = base

async def guest_activate(self):
headers = self.base._base_headers
headers.pop('X-Twitter-Active-User')
headers.pop('X-Twitter-Auth-Type')
headers.pop('X-Twitter-Active-User', None)
headers.pop('X-Twitter-Auth-Type', None)
return await self.base.post(
Endpoint.GUEST_ACTIVATE,
headers=headers,
Expand Down
54 changes: 54 additions & 0 deletions twikit/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,57 @@
'longform_notetweets_inline_media_enabled': True,
'responsive_web_enhance_cards_enabled': False
}

TWEET_RESULT_BY_REST_ID_FEATURES = {
'creator_subscriptions_tweet_preview_api_enabled': True,
'communities_web_enable_tweet_community_results_fetch': True,
'c9s_tweet_anatomy_moderator_badge_enabled': True,
'articles_preview_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,
'freedom_of_speech_not_reach_fetch_enabled': True,
'standardized_nudges_misinfo': True,
'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled': True,
'rweb_video_timestamps_enabled': True,
'longform_notetweets_rich_text_read_enabled': True,
'longform_notetweets_inline_media_enabled': True,
'rweb_tipjar_consumption_enabled': True,
'responsive_web_graphql_exclude_directive_enabled': True,
'verified_phone_label_enabled': False,
'responsive_web_graphql_skip_user_profile_image_extensions_enabled': False,
'responsive_web_graphql_timeline_navigation_enabled': True,
'responsive_web_enhance_cards_enabled': False
}

USER_HIGHLIGHTS_TWEETS_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': 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,
'freedom_of_speech_not_reach_fetch_enabled': True,
'standardized_nudges_misinfo': True,
'tweet_with_visibility_results_prefer_gql_limited_actions_policy_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
}
3 changes: 3 additions & 0 deletions twikit/guest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .client import GuestClient
from .tweet import Tweet
from .user import User
Loading

0 comments on commit 5cdeb53

Please sign in to comment.