Skip to content

Commit 051cfb5

Browse files
authored
feat: update twitter details
feat: update twitter details
2 parents 6c67657 + bddd24c commit 051cfb5

File tree

3 files changed

+78
-31
lines changed

3 files changed

+78
-31
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "twitter-api-client-v2"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
description = "Implementation of X/Twitter v1, v2, and GraphQL APIs."
55
requires-python = ">=3.12"
66
license = "MIT"

twitter/constants.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class Operation:
111111
UserTweetsAndReplies = {'userId': int}, 'RIWc55YCNyUJ-U3HHGYkdg', 'UserTweetsAndReplies'
112112
TweetResultByRestId = {'tweetId': int}, 'd6YKjvQ920F-D4Y1PruO-A', 'TweetResultByRestId'
113113
TweetResultsByRestIds = {'tweetIds': list[int | str]}, 'BWy5aoI-WvwbeSiHUIf2Hw', 'TweetResultsByRestIds'
114-
TweetDetail = {'focalTweetId': int}, 'zXaXQgfyR4GxE21uwYQSyA', 'TweetDetail'
114+
TweetDetail = {'focalTweetId': int}, 'ooUbmy0T2DmvwfjgARktiQ', 'TweetDetail'
115115
TweetStats = {'rest_id': int}, 'EvbTkPDT-xQCfupPu0rWMA', 'TweetStats'
116116
Likes = {'userId': int}, 'nXEl0lfN_XSznVMlprThgQ', 'Likes'
117117
Followers = {'userId': int}, 'pd8Tt1qUz1YWrICegqZ8cw', 'Followers'
@@ -671,26 +671,56 @@ class Operation:
671671
'ext': 'mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,superFollowMetadata,unmentionInfo,editControl'
672672
}
673673

674-
latest_features = {
675-
"responsive_web_grok_share_attachment_enabled": True,
676-
"responsive_web_grok_show_grok_translated_post": True,
677-
"responsive_web_profile_redirect_enabled": True,
678-
"responsive_web_grok_analyze_post_followups_enabled": True,
679-
"post_ctas_fetch_enabled": True,
680-
"rweb_tipjar_consumption_enabled": True,
681-
"responsive_web_grok_analysis_button_from_backend": True,
682-
"responsive_web_grok_imagine_annotation_enabled": True,
683-
"responsive_web_jetfuel_frame": True,
684-
"premium_content_api_read_enabled": True,
685-
"responsive_web_grok_analyze_button_fetch_trends_enabled": True,
686-
"communities_web_enable_tweet_community_results_fetch": True,
687-
"profile_label_improvements_pcf_label_in_post_enabled": True,
688-
"responsive_web_grok_image_annotation_enabled": True,
689-
"responsive_web_grok_community_note_auto_translation_is_enabled": True,
690-
"articles_preview_enabled": True,
691-
"responsive_web_grok_annotations_enabled": True,
692-
"longform_notetweets_inline_media_enabled": True,
693-
"responsive_web_edit_tweet_api_enabled": True,
694-
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
695-
"view_counts_everywhere_api_enabled": True
696-
}
674+
target_features = {
675+
"rweb_video_screen_enabled": False,
676+
"profile_label_improvements_pcf_label_in_post_enabled": True,
677+
"responsive_web_profile_redirect_enabled": False,
678+
"rweb_tipjar_consumption_enabled": False,
679+
"verified_phone_label_enabled": False,
680+
"creator_subscriptions_tweet_preview_api_enabled": True,
681+
"responsive_web_graphql_timeline_navigation_enabled": True,
682+
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
683+
"premium_content_api_read_enabled": False,
684+
"communities_web_enable_tweet_community_results_fetch": True,
685+
"c9s_tweet_anatomy_moderator_badge_enabled": True,
686+
"responsive_web_grok_analyze_button_fetch_trends_enabled": False,
687+
"responsive_web_grok_analyze_post_followups_enabled": True,
688+
"responsive_web_jetfuel_frame": True,
689+
"responsive_web_grok_share_attachment_enabled": True,
690+
"responsive_web_grok_annotations_enabled": True,
691+
"articles_preview_enabled": True,
692+
"responsive_web_edit_tweet_api_enabled": True,
693+
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
694+
"view_counts_everywhere_api_enabled": True,
695+
"longform_notetweets_consumption_enabled": True,
696+
"responsive_web_twitter_article_tweet_consumption_enabled": True,
697+
"tweet_awards_web_tipping_enabled": False,
698+
"responsive_web_grok_show_grok_translated_post": True,
699+
"responsive_web_grok_analysis_button_from_backend": True,
700+
"post_ctas_fetch_enabled": True,
701+
"freedom_of_speech_not_reach_fetch_enabled": True,
702+
"standardized_nudges_misinfo": True,
703+
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
704+
"longform_notetweets_rich_text_read_enabled": True,
705+
"longform_notetweets_inline_media_enabled": True,
706+
"responsive_web_grok_image_annotation_enabled": True,
707+
"responsive_web_grok_imagine_annotation_enabled": True,
708+
"responsive_web_grok_community_note_auto_translation_is_enabled": False,
709+
"responsive_web_enhance_cards_enabled": False}
710+
711+
target_field_toggles = {
712+
"withArticleRichContentState": True,
713+
"withArticlePlainText": False,
714+
"withGrokAnalyze": False,
715+
"withDisallowedReplyControls": False
716+
}
717+
718+
static_variables = {
719+
"with_rux_injections": False,
720+
"rankingMode": "Relevance",
721+
"includePromotedContent": True,
722+
"withCommunity": True,
723+
"withQuickPromoteEligibilityTweetFields": True,
724+
"withBirdwatchNotes": True,
725+
"withVoice": True
726+
}

twitter/scraper.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
if platform.system() != 'Windows':
2626
try:
2727
import uvloop
28+
2829
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
2930
except ImportError as e:
3031
...
@@ -59,7 +60,8 @@ def tweets_by_id(self, tweet_ids: list[int | str], **kwargs) -> list[dict]:
5960
@param kwargs: optional keyword arguments
6061
@return: list of tweet data as dicts
6162
"""
62-
return self._run(Operation.TweetResultByRestId, tweet_ids, **(latest_features | kwargs))
63+
kwargs['features'] = target_features
64+
return self._run(Operation.TweetResultByRestId, tweet_ids, **kwargs)
6365

6466
def tweets_by_ids(self, tweet_ids: list[int | str], **kwargs) -> list[dict]:
6567
"""
@@ -83,6 +85,9 @@ def tweets_details(self, tweet_ids: list[int], **kwargs) -> list[dict]:
8385
@param kwargs: optional keyword arguments
8486
@return: list of tweet data as dicts
8587
"""
88+
kwargs.update(static_variables)
89+
kwargs['features'] = target_features
90+
kwargs['fieldToggles'] = target_field_toggles
8691
return self._run(Operation.TweetDetail, tweet_ids, **kwargs)
8792

8893
def tweets(self, user_ids: list[int], **kwargs) -> list[dict]:
@@ -244,7 +249,8 @@ def users_by_id(self, user_ids: list[int], **kwargs) -> list[dict]:
244249
"""
245250
return self._run(Operation.UserByRestId, user_ids, **kwargs)
246251

247-
def download_media(self, ids: list[int], photos: bool = True, videos: bool = True, cards: bool = True, hq_img_variant: bool = True, video_thumb: bool = False, out: str = 'media',
252+
def download_media(self, ids: list[int], photos: bool = True, videos: bool = True, cards: bool = True,
253+
hq_img_variant: bool = True, video_thumb: bool = False, out: str = 'media',
248254
metadata_out: str = 'media.json', **kwargs) -> dict:
249255
"""
250256
Download and extract media metadata from Tweets
@@ -267,7 +273,8 @@ async def process(fns: Generator) -> list:
267273
'keepalive_expiry': kwargs.pop('keepalive_expiry', 5.0),
268274
}
269275
headers = {'user-agent': random.choice(USER_AGENTS)}
270-
async with AsyncClient(limits=Limits(**limits), headers=headers, http2=True, verify=False, timeout=60, follow_redirects=True) as client:
276+
async with AsyncClient(limits=Limits(**limits), headers=headers, http2=True, verify=False, timeout=60,
277+
follow_redirects=True) as client:
271278
return await tqdm_asyncio.gather(*(fn(client=client) for fn in fns), desc='Downloading Media')
272279

273280
def download(urls: list[tuple], out: str) -> Generator:
@@ -295,7 +302,8 @@ async def get(client: AsyncClient, url: str):
295302
if _id := root.get('rest_id'):
296303
date = root.get('legacy', {}).get('created_at', '')
297304
uid = root.get('legacy', {}).get('user_id_str', '')
298-
media[_id] = {'date': date, 'uid': uid, 'img': set(), 'video': {'thumb': set(), 'video_info': {}, 'hq': set()}, 'card': []}
305+
media[_id] = {'date': date, 'uid': uid, 'img': set(),
306+
'video': {'thumb': set(), 'video_info': {}, 'hq': set()}, 'card': []}
299307
for _media in (y for x in find_key(root, 'media') for y in x if isinstance(x, list)):
300308
if videos:
301309
if vinfo := _media.get('video_info'):
@@ -588,10 +596,18 @@ def _run(self, operation: tuple[dict, str, str], queries: set | list[int | str |
588596

589597
async def _query(self, client: AsyncClient, operation: tuple, **kwargs) -> Response:
590598
keys, qid, name = operation
599+
600+
features_override = kwargs.pop('features', None)
601+
field_toggles = kwargs.pop('fieldToggles', None)
602+
591603
params = {
592604
'variables': Operation.default_variables | keys | kwargs,
593-
'features': Operation.default_features,
605+
'features': features_override if features_override else Operation.default_features,
594606
}
607+
608+
if field_toggles:
609+
params['fieldToggles'] = field_toggles
610+
595611
r = await client.get(f'https://twitter.com/i/api/graphql/{qid}/{name}', params=build_params(params))
596612

597613
try:
@@ -608,7 +624,8 @@ async def _query(self, client: AsyncClient, operation: tuple, **kwargs) -> Respo
608624
async def _process(self, operation: tuple, queries: list[dict], **kwargs):
609625
headers = self.session.headers if self.guest else get_headers(self.session)
610626
cookies = self.session.cookies
611-
async with AsyncClient(limits=Limits(max_connections=MAX_ENDPOINT_LIMIT), headers=headers, cookies=cookies, timeout=20) as c:
627+
async with AsyncClient(limits=Limits(max_connections=MAX_ENDPOINT_LIMIT), headers=headers, cookies=cookies,
628+
timeout=20) as c:
612629
tasks = (self._paginate(c, operation, **q, **kwargs) for q in queries)
613630
if self.pbar:
614631
return await tqdm_asyncio.gather(*tasks, desc=operation[-1])

0 commit comments

Comments
 (0)