Skip to content

feat: publish X Articles from Markdown#65

Open
hhh2210 wants to merge 1 commit into
public-clis:mainfrom
hhh2210:feat/article-publish
Open

feat: publish X Articles from Markdown#65
hhh2210 wants to merge 1 commit into
public-clis:mainfrom
hhh2210:feat/article-publish

Conversation

@hhh2210

@hhh2210 hhh2210 commented Jun 11, 2026

Copy link
Copy Markdown

Summary

Adds a new twitter article-publish command that creates and optionally publishes X Articles from Markdown files.

What changed

  • Add twitter_cli.article.article_markdown_to_content_state() for a conservative Markdown-to-X-Article content_state conversion.
  • Add Article GraphQL operations:
    • ArticleEntityDraftCreate
    • ArticleEntityUpdateTitle
    • ArticleEntityUpdateContent
    • ArticleEntityPublish
  • Add TwitterClient.create_article() to create a draft, update title/content, and publish unless --draft is requested.
  • Add twitter article-publish ARTICLE.md --title "Title" with --draft, --json, and --yaml support.
  • Document the command in English and Chinese README sections.

Supported Markdown subset

This intentionally starts with a text-first subset:

  • paragraphs
  • # / ## headings
  • unordered and ordered list items
  • blockquotes
  • horizontal rules
  • fenced code blocks as Article MARKDOWN atomic blocks
  • inline Markdown links as Article LINK entities

It 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_cli
  • uv run pytest -q
  • uv run twitter --help | rg "article|article-publish"
  • uv run python -m twitter_cli.cli article-publish --help

I 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.

Copilot AI review requested due to automatic review settings June 11, 2026 15:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread twitter_cli/client.py
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 thread twitter_cli/client.py
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 thread tests/test_article.py
Comment on lines +44 to +53
assert content_state["blocks"] == [
{
"data": {},
"text": "",
"key": "48583",
"type": "unstyled",
"entity_ranges": [],
"inline_style_ranges": [],
}
]
Comment thread twitter_cli/article.py
Comment on lines +10 to +12
DraftBlock = dict[str, Any]
DraftEntity = dict[str, Any]
DraftContentState = dict[str, Any]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants