fix cli article output and reply-to parsing

This commit is contained in:
jackwener
2026-03-17 22:53:07 +08:00
parent ad40848c18
commit 9b854e6aa6
3 changed files with 78 additions and 8 deletions

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "twitter-cli"
version = "0.8.5"
version = "0.8.6"
description = "A CLI for Twitter/X — feed, bookmarks, and user timeline in terminal"
readme = "README.md"
license = "Apache-2.0"

View File

@@ -228,6 +228,40 @@ def test_cli_article_markdown_overrides_auto_structured_output(monkeypatch) -> N
assert result.output == article_to_markdown(article)
def test_cli_article_json_output_file_uses_structured_format(monkeypatch, tmp_path) -> None:
article = Tweet(
id="12345",
text="https://t.co/article",
author=Author(id="u1", name="Alice", screen_name="alice"),
metrics=Metrics(likes=1, retweets=2, replies=3, views=4, bookmarks=5),
created_at="2026-03-11",
article_title="Title",
article_text="Hello\n\n## Section",
)
class FakeClient:
def fetch_article(self, tweet_id: str) -> Tweet:
assert tweet_id == "12345"
return article
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None, quiet=False: FakeClient())
monkeypatch.setattr("twitter_cli.cli.load_config", lambda: {})
output_path = tmp_path / "article.json"
runner = CliRunner()
result = runner.invoke(
cli,
["article", "12345", "--json", "--output", str(output_path)],
)
assert result.exit_code == 0
stdout_payload = yaml.safe_load(result.output)
assert stdout_payload["ok"] is True
saved_payload = json.loads(output_path.read_text(encoding="utf-8"))
assert saved_payload["id"] == "12345"
assert saved_payload["articleTitle"] == "Title"
def test_cli_article_rejects_compact_mode() -> None:
runner = CliRunner()
@@ -450,6 +484,26 @@ def test_cli_post_json_output(monkeypatch) -> None:
assert payload["data"]["id"] == "999"
def test_cli_post_reply_to_accepts_status_url(monkeypatch) -> None:
calls = []
class FakeClient:
def create_tweet(self, text: str, reply_to_id=None, media_ids=None) -> str:
calls.append({"text": text, "reply_to_id": reply_to_id})
return "999"
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None, quiet=False: FakeClient())
runner = CliRunner()
result = runner.invoke(
cli,
["post", "hello", "--reply-to", "https://x.com/alice/status/12345?s=20"],
)
assert result.exit_code == 0
assert calls == [{"text": "hello", "reply_to_id": "12345"}]
def test_cli_like_yaml_output(monkeypatch) -> None:
class FakeClient:
def like_tweet(self, tweet_id: str) -> bool:

View File

@@ -32,6 +32,7 @@ Write commands:
from __future__ import annotations
import json
import logging
import re
import sys
@@ -42,6 +43,7 @@ from typing import Any, Callable, Dict, List, Optional
import click
from rich.console import Console
import yaml
from . import __version__
from .auth import get_cookies
@@ -935,7 +937,8 @@ def article(ctx, tweet_id, as_json, as_yaml, as_markdown, output_file):
tweet_id = _normalize_tweet_id(tweet_id)
config = load_config()
rich_output = use_rich_output(as_json=as_json, as_yaml=as_yaml, compact=False) and not as_markdown
mode = _structured_mode(as_json=as_json, as_yaml=as_yaml)
rich_output = (mode is None) and not as_markdown
try:
client = _get_client(config, quiet=not rich_output)
if rich_output:
@@ -948,16 +951,28 @@ def article(ctx, tweet_id, as_json, as_yaml, as_markdown, output_file):
except (TwitterError, RuntimeError) as exc:
_exit_with_error(exc)
article_data = tweet_to_dict(article_tweet)
markdown = article_to_markdown(article_tweet)
if output_file:
Path(output_file).write_text(markdown, encoding="utf-8")
if as_markdown or mode is None:
rendered_output = markdown
elif mode == "json":
rendered_output = json.dumps(article_data, ensure_ascii=False, indent=2)
else:
rendered_output = yaml.safe_dump(
article_data,
allow_unicode=True,
sort_keys=False,
default_flow_style=False,
)
Path(output_file).write_text(rendered_output, encoding="utf-8")
if rich_output:
console.print("💾 Saved article Markdown to %s\n" % output_file)
console.print("💾 Saved article output to %s\n" % output_file)
if as_markdown:
click.echo(markdown, nl=False)
return
if emit_structured(tweet_to_dict(article_tweet), as_json=as_json, as_yaml=as_yaml):
if emit_structured(article_data, as_json=as_json, as_yaml=as_yaml):
return
print_article(article_tweet, console)
@@ -1098,12 +1113,13 @@ def post(text, reply_to, images, as_json, as_yaml):
twitter post "Hello!" --image photo.jpg
twitter post "Gallery" -i a.png -i b.png -i c.jpg
"""
action = "Replying to %s" % reply_to if reply_to else "Posting tweet"
normalized_reply_to = _normalize_tweet_id(reply_to) if reply_to else None
action = "Replying to %s" % normalized_reply_to if normalized_reply_to else "Posting tweet"
rich_output = not _structured_mode(as_json=as_json, as_yaml=as_yaml)
def operation(client: TwitterClient) -> WritePayload:
media_ids = _upload_images(client, images, rich_output=rich_output)
tweet_id = client.create_tweet(text, reply_to_id=reply_to, media_ids=media_ids or None)
tweet_id = client.create_tweet(text, reply_to_id=normalized_reply_to, media_ids=media_ids or None)
return {"success": True, "action": "post", "id": tweet_id, "url": "https://x.com/i/status/%s" % tweet_id}
payload = _run_write_command(
@@ -1112,7 +1128,7 @@ def post(text, reply_to, images, as_json, as_yaml):
operation=operation,
progress_lines=["✏️ %s..." % action],
success_lines=["[green]✅ Tweet posted![/green]"],
error_details={"action": "post", "replyTo": reply_to},
error_details={"action": "post", "replyTo": normalized_reply_to},
)
if payload and not _structured_mode(as_json=as_json, as_yaml=as_yaml):
console.print("🔗 %s" % payload["url"])