diff --git a/pyproject.toml b/pyproject.toml index 45b8eca..10784a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "twitter-cli" -version = "0.4.2" +version = "0.4.3" description = "A CLI for Twitter/X — feed, bookmarks, and user timeline in terminal" readme = "README.md" license = "Apache-2.0" diff --git a/tests/test_client.py b/tests/test_client.py index d31ba38..48b9af9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -213,30 +213,28 @@ class TestBestChromeTarget: # ── _update_features_from_html ─────────────────────────────────────────── class TestUpdateFeaturesFromHtml: - def test_extracts_feature_flags(self): - # Save original state + def test_updates_existing_feature_flags(self): + """Should update existing FEATURES keys, not add new ones.""" original = dict(FEATURES) try: - html = ''' - "responsive_web_test_feature":{"value":true}, - "responsive_web_another_feature":{"value":false}, - "rweb_some_flag":{"value":true} - ''' + # Use a key that exists in FEATURES + existing_key = list(FEATURES.keys())[0] + original_value = FEATURES[existing_key] + opposite = "false" if original_value else "true" + html = '"%s":{"value":%s}' % (existing_key, opposite) _update_features_from_html(html) - assert FEATURES["responsive_web_test_feature"] is True - assert FEATURES["responsive_web_another_feature"] is False - assert FEATURES["rweb_some_flag"] is True + assert FEATURES[existing_key] != original_value finally: - # Restore original state FEATURES.clear() FEATURES.update(original) - def test_ignores_non_feature_keys(self): + def test_does_not_add_new_keys(self): + """Should never add keys not already in FEATURES (prevents URL bloat).""" original = dict(FEATURES) try: - html = '"some_random_key":{"value":true}' + html = '"responsive_web_brand_new_feature":{"value":true}' _update_features_from_html(html) - assert "some_random_key" not in FEATURES + assert "responsive_web_brand_new_feature" not in FEATURES finally: FEATURES.clear() FEATURES.update(original) diff --git a/twitter_cli/client.py b/twitter_cli/client.py index de10d0e..05501ed 100644 --- a/twitter_cli/client.py +++ b/twitter_cli/client.py @@ -53,43 +53,24 @@ TWITTER_OPENAPI_URL = ( "main/src/config/placeholder.json" ) +# Essential features only — keep this list SMALL to avoid 414/431 URI Too Long. +# Twitter's API defaults missing features to False, so we only need True-valued ones +# that affect tweet data we actually consume. Each additional key adds ~60 chars to URL. _DEFAULT_FEATURES = { - "rweb_video_screen_enabled": False, - "profile_label_improvements_pcf_label_in_post_enabled": True, - "responsive_web_profile_redirect_enabled": False, - "rweb_tipjar_consumption_enabled": False, - "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, - "premium_content_api_read_enabled": False, "communities_web_enable_tweet_community_results_fetch": True, "c9s_tweet_anatomy_moderator_badge_enabled": True, - "responsive_web_grok_analyze_button_fetch_trends_enabled": False, - "responsive_web_grok_analyze_post_followups_enabled": True, - "responsive_web_jetfuel_frame": True, - "responsive_web_grok_share_attachment_enabled": True, - "responsive_web_grok_annotations_enabled": True, "articles_preview_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, - "content_disclosure_indicator_enabled": True, - "content_disclosure_ai_generated_indicator_enabled": True, - "responsive_web_grok_show_grok_translated_post": True, - "responsive_web_grok_analysis_button_from_backend": True, - "post_ctas_fetch_enabled": True, - "freedom_of_speech_not_reach_fetch_enabled": True, - "standardized_nudges_misinfo": True, "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True, "longform_notetweets_rich_text_read_enabled": True, - "longform_notetweets_inline_media_enabled": False, - "responsive_web_grok_image_annotation_enabled": True, - "responsive_web_grok_imagine_annotation_enabled": True, - "responsive_web_grok_community_note_auto_translation_is_enabled": False, + "freedom_of_speech_not_reach_fetch_enabled": True, + "standardized_nudges_misinfo": True, + "responsive_web_graphql_timeline_navigation_enabled": True, "responsive_web_enhance_cards_enabled": False, } @@ -226,11 +207,9 @@ def _update_features_from_html(html): Twitter embeds feature switch config in inline scripts on the homepage. We parse these to keep FEATURES in sync with the current frontend. + Only UPDATES existing keys — never adds new ones to avoid URL bloat. """ try: - # Look for feature flags in inline script content - # Pattern: "featureSwitch":{"...":{"value":true/false},...} - # Also try: features:{key:!0, key2:!1, ...} in JS bundles feature_pattern = re.compile( r'"([a-z][a-z0-9_]+)":\s*\{\s*"value"\s*:\s*(true|false)', re.IGNORECASE, @@ -239,8 +218,10 @@ def _update_features_from_html(html): for match in feature_pattern.finditer(html): key = match.group(1) value = match.group(2).lower() == "true" - # Only update keys that look like feature flags - if any(prefix in key for prefix in ("responsive_web_", "rweb_", "longform_", "creator_", "communities_", "c9s_")): + # Only update keys already in FEATURES — never add new ones + # Adding new keys inflates URL length, causing 414/431 errors + if key in FEATURES and FEATURES[key] != value: + logger.debug("Feature flag updated: %s = %s -> %s", key, FEATURES[key], value) FEATURES[key] = value found += 1 if found: diff --git a/uv.lock b/uv.lock index fe72ff6..c36b806 100644 --- a/uv.lock +++ b/uv.lock @@ -950,7 +950,7 @@ wheels = [ [[package]] name = "twitter-cli" -version = "0.4.1" +version = "0.4.2" source = { editable = "." } dependencies = [ { name = "beautifulsoup4" },