feat: support long-form tweets via CreateNoteTweet#60
Open
rakei076 wants to merge 1 commit into
Open
Conversation
…s#54) Adds automatic routing to the CreateNoteTweet GraphQL endpoint when the weighted character count of a post exceeds 280 (X Premium long-form post limit). Standard tweets continue to use CreateTweet. Changes ------- * graphql.py: register CreateNoteTweet queryId and add the four feature flags required by the long-form endpoint (longform_notetweets_creation_enabled, longform_notetweets_richtext_consumption_enabled, responsive_web_twitter_article_data_v2_enabled, articles_preview_enabled, tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled) * client.py: add static helper _weighted_length() that mirrors X's weighted-character rule (ASCII = 1, others = 2), and route create_tweet() to CreateNoteTweet when weighted > 280. Notes on the silent-failure bug ------------------------------- CreateNoteTweet requires variables.disallowed_reply_options to be present (null is the canonical value). Without this field X responds HTTP 200 with an empty tweet_results object, which makes the failure look like a successful call. Setting disallowed_reply_options=None fixes the silent failure. Tested on an X Premium account with Japanese long-form posts (~400 weighted characters): tweets are created and returned with a valid rest_id. Standard <=280 posts continue to use the existing CreateTweet path with no behavior change.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds support for X long-form “Note Tweets” by introducing the CreateNoteTweet GraphQL operation and routing oversized tweets to that endpoint.
Changes:
- Added
CreateNoteTweetGraphQL operation id and enabled additional long-form/article feature flags. - Implemented weighted-length calculation and auto-routing in
create_tweetto useCreateNoteTweetwhen over the limit. - Updated result parsing and error message to account for multiple mutations.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| twitter_cli/graphql.py | Registers CreateNoteTweet operation and enables feature flags needed for long-form/article-related GraphQL flows. |
| twitter_cli/client.py | Adds weighted-length logic and routes tweet creation to either CreateTweet or CreateNoteTweet; adjusts response parsing and errors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+563
to
+575
| def _weighted_length(text): | ||
| # type: (str) -> int | ||
| """Return X's weighted character count for a tweet. | ||
|
|
||
| X counts basic ASCII characters as 1 and most other characters (CJK, | ||
| emoji, etc.) as 2. The standard tweet limit is 280 weighted units; | ||
| anything above must go through the CreateNoteTweet endpoint (X | ||
| Premium long-form post). | ||
| """ | ||
| n = 0 | ||
| for ch in text: | ||
| n += 1 if ord(ch) < 0x80 else 2 | ||
| return n |
Comment on lines
+604
to
+616
| # Route to long-form endpoint if the tweet exceeds the standard limit. | ||
| weighted = self._weighted_length(text) | ||
| if weighted > 280: | ||
| op_name = "CreateNoteTweet" | ||
| # CreateNoteTweet requires this field; without it X returns | ||
| # HTTP 200 with an empty tweet_results object (silent failure). | ||
| variables["disallowed_reply_options"] = None | ||
| logger.info( | ||
| "Tweet weighted=%d > 280, using CreateNoteTweet (long-form)", | ||
| weighted, | ||
| ) | ||
| else: | ||
| op_name = "CreateTweet" |
Comment on lines
+581
to
+582
| Automatically routes to the CreateNoteTweet GraphQL endpoint when the | ||
| weighted character count exceeds 280 (X Premium long-form post). |
Comment on lines
+604
to
+607
| # Route to long-form endpoint if the tweet exceeds the standard limit. | ||
| weighted = self._weighted_length(text) | ||
| if weighted > 280: | ||
| op_name = "CreateNoteTweet" |
Comment on lines
+620
to
+625
| if op_name == "CreateNoteTweet": | ||
| result = _deep_get( | ||
| data, "data", "notetweet_create", "tweet_results", "result" | ||
| ) | ||
| else: | ||
| result = _deep_get(data, "data", "create_tweet", "tweet_results", "result") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #54
Summary
Adds automatic routing to the
CreateNoteTweetGraphQL endpoint when the weighted character count of a post exceeds 280, enabling X Premium users to post long-form tweets from the CLI. Standard tweets (≤ 280 weighted) continue to useCreateTweetwith no behavior change.Changes
twitter_cli/graphql.pyCreateNoteTweetqueryId; add 5 feature flags required by the long-form endpointtwitter_cli/client.py_weighted_length()helper (mirrors X's ASCII=1 / other=2 rule); routecreate_tweet()toCreateNoteTweetwhen weighted > 280Total diff: +47 / −3 across 2 files.
The silent-failure bug
CreateNoteTweetrequiresvariables.disallowed_reply_optionsto be present (withnullas the canonical value). Without this field X responds HTTP 200 with an emptytweet_resultsobject — the call looks successful but no tweet is created. This is likely why the endpoint hasn't been wired up before: the failure mode is invisible without comparing responses against a successful one.Setting
variables["disallowed_reply_options"] = Noneresolves it; the response then contains the fulltweet_results.result.rest_idas expected.Testing
Verified on an X Premium account with Japanese long-form posts (~400 weighted characters):
Standard ASCII tweets ≤ 280 weighted still route through
CreateTweet— no regression.Notes
CreateNoteTweetqueryId (yeInFtqpUoABoBE_YWPYgA) was extracted from x.com's current JS bundle and added toFALLBACK_QUERY_IDS; the existing_scan_bundlesflow will keep it fresh if X rotates the hash.