feat: support Premium long-form posts#64
Conversation
|
Article publishing note: this PR intentionally only fixes long-form posts. X Articles use a separate entity flow ( |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds long-form (“Note Tweet”) support by routing oversized tweet text to X’s CreateNoteTweet GraphQL mutation, while updating feature flags, tests, and user-facing docs.
Changes:
- Add
CreateNoteTweetoperation + long-form related feature flags in GraphQL config. - Route
create_tweet/quote_tweettoCreateNoteTweetwhen text exceeds the weighted length threshold; parse results from multiple response shapes. - Add unit tests and README documentation for long-form routing behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| twitter_cli/graphql.py | Adds CreateNoteTweet operation id and enables long-form/article-related feature flags. |
| twitter_cli/client.py | Implements weighted-length routing to CreateNoteTweet, shared result extraction, and improved error messages. |
| tests/test_client.py | Adds coverage for routing logic and asserts the chosen mutation name/variables. |
| README.md | Documents automatic routing for Premium long-form posts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _tweet_weighted_length(text): | ||
| # type: (str) -> int | ||
| """Return X's approximate weighted character count for composer routing.""" | ||
| return sum(1 if ord(ch) < 0x80 else 2 for ch in text) | ||
|
|
||
|
|
||
| def _tweet_create_operation(text): | ||
| # type: (str) -> str | ||
| """Return the GraphQL mutation for a standard or long-form post.""" | ||
| if _tweet_weighted_length(text) > _STANDARD_TWEET_WEIGHT_LIMIT: | ||
| return "CreateNoteTweet" | ||
| return "CreateTweet" |
| "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True, | ||
| "view_counts_everywhere_api_enabled": True, | ||
| "longform_notetweets_consumption_enabled": True, | ||
| "longform_notetweets_creation_enabled": True, | ||
| "longform_notetweets_richtext_consumption_enabled": True, | ||
| "responsive_web_twitter_article_tweet_consumption_enabled": True, | ||
| "articles_preview_enabled": True, | ||
| "tweet_awards_web_tipping_enabled": False, | ||
| "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True, | ||
| "longform_notetweets_rich_text_read_enabled": True, | ||
| "longform_notetweets_inline_media_enabled": True, |
| operation_name = _tweet_create_operation(text) | ||
| if operation_name == "CreateNoteTweet": | ||
| # Required by the long-form mutation. Without it X can return HTTP | ||
| # 200 with an empty tweet_results object instead of creating a post. | ||
| variables["disallowed_reply_options"] = None | ||
| data = self._graphql_post(operation_name, variables, FEATURES) |
|
|
||
| def _tweet_weighted_length(text): | ||
| # type: (str) -> int | ||
| """Return X's approximate weighted character count for composer routing.""" |
|
#60 by @rakei076 reached the same core conclusion 17 days earlier — including the What this PR adds on top of #60:
Happy to either rebase this as a follow-up on top of #60, or have maintainers pick whichever — the quote path and the tests are the parts worth landing either way. |
Summary
Fixes #54.
Adds automatic routing to X's long-form post mutation for Premium accounts when a post exceeds the standard 280 weighted-character limit.
What changed
CreateNoteTweetin GraphQL fallback query IDs.create_tweet()andquote_tweet()toCreateNoteTweetwhen text is over the standard weighted limit.disallowed_reply_options: nullfor long-form writes; without this field X can return an emptytweet_resultsobject.CreateTweetpath.Why this scope
This intentionally keeps the patch narrower than #62: it does not add video upload or Article publishing. Article publishing appears to be a separate X entity flow (
ArticleEntityDraftCreate/ update title / update content / publish) and should be handled as a separate feature with its own payload tests.Compared with #60, this also covers quote tweets and includes local tests for the routing and payload shape.
Validation
uv run ruff check .uv run mypy twitter_cliuv run pytest -q