feat: publish X Articles from Markdown#65
Open
hhh2210 wants to merge 1 commit into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds the ability to publish X Articles from Markdown via the CLI by introducing an article Markdown-to-content_state serializer, new GraphQL operations/feature flags, and client + CLI plumbing.
Changes:
- Add GraphQL operation IDs + dedicated feature flags for article creation/update/publish.
- Implement
TwitterClient.create_article(...)workflow (draft → set title/content → optional publish). - Add
twitter article-publish <file.md> --title ... [--draft]CLI command and supporting Markdown conversion helpers + tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| twitter_cli/graphql.py | Adds fallback GraphQL operation IDs and a dedicated ARTICLE_FEATURES flag set for article mutations. |
| twitter_cli/client.py | Implements article draft creation, title/content updates, and publishing via GraphQL. |
| twitter_cli/cli.py | Adds article-publish command to publish/draft an article from a Markdown file. |
| twitter_cli/article.py | New Markdown-to-article content_state conversion helper. |
| tests/test_client.py | Adds unit tests for create_article flow and GraphQL call sequence. |
| tests/test_cli.py | Adds CLI tests for JSON/YAML output and draft mode behavior. |
| tests/test_article.py | Adds tests for Markdown conversion into blocks/entities. |
| README.md | Documents the new article publishing capability and command usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+492
to
+501
| def _update_article_content(self, article_id, content_state): | ||
| # type: (str, Dict[str, Any]) -> None | ||
| variables = { | ||
| "content_state": { | ||
| "blocks": content_state.get("blocks") or [], | ||
| "entity_map": content_state.get("entity_map") or [], | ||
| }, | ||
| "article_entity": article_id, | ||
| } | ||
| self._graphql_post("ArticleEntityUpdateContent", variables, ARTICLE_FEATURES) |
Comment on lines
+441
to
+464
| def create_article(self, title, content_state, publish=True): | ||
| # type: (str, Dict[str, Any], bool) -> Dict[str, Any] | ||
| """Create an X Article draft, optionally publish it, and return metadata.""" | ||
| if not title.strip(): | ||
| raise TwitterAPIError(0, "Article title is required") | ||
|
|
||
| article_id = self._create_article_draft() | ||
| self._update_article_title(article_id, title) | ||
| self._update_article_content(article_id, content_state) | ||
| if publish: | ||
| published_id = self._publish_article(article_id) | ||
| if published_id: | ||
| article_id = published_id | ||
| url = "https://x.com/i/article/%s" % article_id | ||
| else: | ||
| url = "https://x.com/compose/article/edit/%s" % article_id | ||
|
|
||
| self._write_delay() | ||
| return { | ||
| "id": article_id, | ||
| "title": title, | ||
| "url": url, | ||
| "published": bool(publish), | ||
| } |
Comment on lines
+44
to
+53
| assert content_state["blocks"] == [ | ||
| { | ||
| "data": {}, | ||
| "text": "", | ||
| "key": "48583", | ||
| "type": "unstyled", | ||
| "entity_ranges": [], | ||
| "inline_style_ranges": [], | ||
| } | ||
| ] |
Comment on lines
+10
to
+12
| DraftBlock = dict[str, Any] | ||
| DraftEntity = dict[str, Any] | ||
| DraftContentState = dict[str, Any] |
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.
Summary
Adds a new
twitter article-publishcommand that creates and optionally publishes X Articles from Markdown files.What changed
twitter_cli.article.article_markdown_to_content_state()for a conservative Markdown-to-X-Articlecontent_stateconversion.ArticleEntityDraftCreateArticleEntityUpdateTitleArticleEntityUpdateContentArticleEntityPublishTwitterClient.create_article()to create a draft, update title/content, and publish unless--draftis requested.twitter article-publish ARTICLE.md --title "Title"with--draft,--json, and--yamlsupport.Supported Markdown subset
This intentionally starts with a text-first subset:
#/##headingsIt does not attempt image upload, cover media, tables, or full Draft.js parity in this PR.
Validation
uv run ruff check .uv run mypy twitter_cliuv run pytest -quv run twitter --help | rg "article|article-publish"uv run python -m twitter_cli.cli article-publish --helpI did not run a live publish from the PR branch because publishing an Article creates account content and this patch intentionally does not add an Article delete command.