feat: add twitter article markdown command (#16)
This commit is contained in:
14
README.md
14
README.md
@@ -24,6 +24,7 @@ A terminal-first CLI for Twitter/X: read timelines, bookmarks, and user profiles
|
|||||||
- Bookmarks: list saved tweets from your account
|
- Bookmarks: list saved tweets from your account
|
||||||
- Search: find tweets by keyword with Top/Latest/Photos/Videos tabs
|
- Search: find tweets by keyword with Top/Latest/Photos/Videos tabs
|
||||||
- Tweet detail: view a tweet and its replies
|
- Tweet detail: view a tweet and its replies
|
||||||
|
- Article: fetch a Twitter Article and export it as Markdown
|
||||||
- List timeline: fetch tweets from a Twitter List
|
- List timeline: fetch tweets from a Twitter List
|
||||||
- User lookup: fetch user profile, tweets, likes, followers, and following
|
- User lookup: fetch user profile, tweets, likes, followers, and following
|
||||||
- `--full-text`: disable tweet text truncation in rich table output
|
- `--full-text`: disable tweet text truncation in rich table output
|
||||||
@@ -117,6 +118,12 @@ twitter tweet 1234567890
|
|||||||
twitter tweet 1234567890 --full-text
|
twitter tweet 1234567890 --full-text
|
||||||
twitter tweet https://x.com/user/status/1234567890
|
twitter tweet https://x.com/user/status/1234567890
|
||||||
|
|
||||||
|
# Twitter Article
|
||||||
|
twitter article 1234567890
|
||||||
|
twitter article https://x.com/user/article/1234567890 --json
|
||||||
|
twitter article 1234567890 --markdown
|
||||||
|
twitter article 1234567890 --output article.md
|
||||||
|
|
||||||
# List timeline
|
# List timeline
|
||||||
twitter list 1539453138322673664
|
twitter list 1539453138322673664
|
||||||
twitter list 1539453138322673664 --full-text
|
twitter list 1539453138322673664 --full-text
|
||||||
@@ -344,6 +351,7 @@ After installation, OpenClaw can call `twitter-cli` commands directly.
|
|||||||
- 收藏读取:查看账号书签推文
|
- 收藏读取:查看账号书签推文
|
||||||
- 搜索:按关键词搜索推文,支持 Top/Latest/Photos/Videos
|
- 搜索:按关键词搜索推文,支持 Top/Latest/Photos/Videos
|
||||||
- 推文详情:查看推文及其回复
|
- 推文详情:查看推文及其回复
|
||||||
|
- 文章读取:获取 Twitter 长文,并导出为 Markdown
|
||||||
- 列表时间线:获取 Twitter List 的推文
|
- 列表时间线:获取 Twitter List 的推文
|
||||||
- 用户查询:查看用户资料、推文、点赞、粉丝和关注
|
- 用户查询:查看用户资料、推文、点赞、粉丝和关注
|
||||||
- `--full-text`:在 rich table 输出里关闭推文正文截断
|
- `--full-text`:在 rich table 输出里关闭推文正文截断
|
||||||
@@ -408,6 +416,12 @@ twitter search "trending" --filter # 启用排序筛选
|
|||||||
twitter tweet 1234567890
|
twitter tweet 1234567890
|
||||||
twitter tweet 1234567890 --full-text
|
twitter tweet 1234567890 --full-text
|
||||||
|
|
||||||
|
# Twitter 长文
|
||||||
|
twitter article 1234567890
|
||||||
|
twitter article https://x.com/user/article/1234567890 --json
|
||||||
|
twitter article 1234567890 --markdown
|
||||||
|
twitter article 1234567890 --output article.md
|
||||||
|
|
||||||
# 列表时间线
|
# 列表时间线
|
||||||
twitter list 1539453138322673664
|
twitter list 1539453138322673664
|
||||||
twitter list 1539453138322673664 --full-text
|
twitter list 1539453138322673664 --full-text
|
||||||
|
|||||||
14
SCHEMA.md
14
SCHEMA.md
@@ -25,10 +25,24 @@ error:
|
|||||||
- `--yaml` and `--json` both use this envelope
|
- `--yaml` and `--json` both use this envelope
|
||||||
- non-TTY stdout defaults to YAML
|
- non-TTY stdout defaults to YAML
|
||||||
- tweet and user lists are returned under `data`
|
- tweet and user lists are returned under `data`
|
||||||
|
- `article` returns a single tweet object under `data`
|
||||||
- `status` returns `data.authenticated` plus `data.user`
|
- `status` returns `data.authenticated` plus `data.user`
|
||||||
- `whoami` returns `data.user`
|
- `whoami` returns `data.user`
|
||||||
- write commands also support explicit `--json` / `--yaml`
|
- write commands also support explicit `--json` / `--yaml`
|
||||||
|
|
||||||
|
## Article Fields
|
||||||
|
|
||||||
|
`twitter article <id> --json` returns the standard tweet object plus:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
data:
|
||||||
|
id: "1234567890"
|
||||||
|
articleTitle: "Article Title"
|
||||||
|
articleText: |
|
||||||
|
# Heading
|
||||||
|
Body text...
|
||||||
|
```
|
||||||
|
|
||||||
## Error Codes
|
## Error Codes
|
||||||
|
|
||||||
Common structured error codes:
|
Common structured error codes:
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ def tweet_factory():
|
|||||||
retweeted_by=overrides.pop("retweeted_by", None),
|
retweeted_by=overrides.pop("retweeted_by", None),
|
||||||
quoted_tweet=overrides.pop("quoted_tweet", None),
|
quoted_tweet=overrides.pop("quoted_tweet", None),
|
||||||
score=overrides.pop("score", 0.0),
|
score=overrides.pop("score", 0.0),
|
||||||
|
article_title=overrides.pop("article_title", None),
|
||||||
|
article_text=overrides.pop("article_text", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
return _make_tweet
|
return _make_tweet
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from rich.console import Console
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from twitter_cli.cli import cli
|
from twitter_cli.cli import cli
|
||||||
from twitter_cli.formatter import print_tweet_table
|
from twitter_cli.formatter import article_to_markdown, print_tweet_table
|
||||||
from twitter_cli.models import UserProfile
|
from twitter_cli.models import Author, Metrics, Tweet, UserProfile
|
||||||
from twitter_cli.serialization import tweets_to_json
|
from twitter_cli.serialization import tweets_to_json
|
||||||
|
|
||||||
|
|
||||||
@@ -125,6 +125,104 @@ def test_cli_tweet_accepts_shared_url_with_query(monkeypatch) -> None:
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_article_accepts_article_url_and_json(monkeypatch) -> None:
|
||||||
|
class FakeClient:
|
||||||
|
def fetch_article(self, tweet_id: str) -> Tweet:
|
||||||
|
assert tweet_id == "12345"
|
||||||
|
return 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",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"twitter_cli.cli.load_config",
|
||||||
|
lambda: {"fetch": {"count": 50}, "filter": {}, "rateLimit": {}},
|
||||||
|
)
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["article", "https://x.com/user/article/12345?s=20", "--json"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
payload = yaml.safe_load(result.output)
|
||||||
|
assert payload["ok"] is True
|
||||||
|
assert payload["data"]["id"] == "12345"
|
||||||
|
assert payload["data"]["articleTitle"] == "Title"
|
||||||
|
assert "Hello" in payload["data"]["articleText"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_article_markdown_output_and_save(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: FakeClient())
|
||||||
|
monkeypatch.setattr("twitter_cli.cli.load_config", lambda: {})
|
||||||
|
output_path = tmp_path / "article.md"
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
["article", "12345", "--markdown", "--output", str(output_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == article_to_markdown(article)
|
||||||
|
assert output_path.read_text(encoding="utf-8") == article_to_markdown(article)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_article_markdown_overrides_auto_structured_output(monkeypatch) -> 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.setenv("OUTPUT", "auto")
|
||||||
|
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
|
||||||
|
monkeypatch.setattr("twitter_cli.cli.load_config", lambda: {})
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["article", "12345", "--markdown"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output == article_to_markdown(article)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_article_rejects_compact_mode() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["-c", "article", "12345"])
|
||||||
|
|
||||||
|
assert result.exit_code == 2
|
||||||
|
assert "does not support --compact" in result.output
|
||||||
|
|
||||||
|
|
||||||
def test_cli_bookmark_alias_works(monkeypatch) -> None:
|
def test_cli_bookmark_alias_works(monkeypatch) -> None:
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
|
|||||||
@@ -49,3 +49,16 @@ def test_compact_serialization(tweet_factory) -> None:
|
|||||||
assert len(parsed) == 1
|
assert len(parsed) == 1
|
||||||
assert parsed[0]["author"] == "@alice"
|
assert parsed[0]["author"] == "@alice"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tweet_roundtrip_preserves_article_fields(tweet_factory) -> None:
|
||||||
|
tweet = tweet_factory(
|
||||||
|
"88",
|
||||||
|
article_title="Long-form title",
|
||||||
|
article_text="Intro\n\n## Details",
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = tweet_to_dict(tweet)
|
||||||
|
restored = tweet_from_dict(payload)
|
||||||
|
|
||||||
|
assert restored.article_title == "Long-form title"
|
||||||
|
assert restored.article_text == "Intro\n\n## Details"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Read commands:
|
|||||||
twitter user-posts elonmusk # user tweets
|
twitter user-posts elonmusk # user tweets
|
||||||
twitter likes elonmusk # user likes
|
twitter likes elonmusk # user likes
|
||||||
twitter tweet <id> # tweet detail + replies
|
twitter tweet <id> # tweet detail + replies
|
||||||
|
twitter article <id> # Twitter Article as Markdown
|
||||||
twitter list <id> # list timeline
|
twitter list <id> # list timeline
|
||||||
twitter followers <handle> # followers list
|
twitter followers <handle> # followers list
|
||||||
twitter following <handle> # following list
|
twitter following <handle> # following list
|
||||||
@@ -46,7 +47,9 @@ from .client import TwitterClient
|
|||||||
from .config import load_config
|
from .config import load_config
|
||||||
from .filter import filter_tweets
|
from .filter import filter_tweets
|
||||||
from .formatter import (
|
from .formatter import (
|
||||||
|
article_to_markdown,
|
||||||
print_filter_stats,
|
print_filter_stats,
|
||||||
|
print_article,
|
||||||
print_tweet_detail,
|
print_tweet_detail,
|
||||||
print_tweet_table,
|
print_tweet_table,
|
||||||
print_user_profile,
|
print_user_profile,
|
||||||
@@ -63,6 +66,7 @@ from .output import (
|
|||||||
use_rich_output,
|
use_rich_output,
|
||||||
)
|
)
|
||||||
from .serialization import (
|
from .serialization import (
|
||||||
|
tweet_to_dict,
|
||||||
tweets_from_json,
|
tweets_from_json,
|
||||||
tweets_to_data,
|
tweets_to_data,
|
||||||
tweets_to_compact_json,
|
tweets_to_compact_json,
|
||||||
@@ -214,7 +218,7 @@ def _normalize_tweet_id(value):
|
|||||||
candidate = raw
|
candidate = raw
|
||||||
if parsed.scheme and parsed.netloc:
|
if parsed.scheme and parsed.netloc:
|
||||||
path = parsed.path.rstrip("/")
|
path = parsed.path.rstrip("/")
|
||||||
match = re.search(r"/status/(\d+)$", path)
|
match = re.search(r"/(?:status|article)/(\d+)$", path)
|
||||||
if not match:
|
if not match:
|
||||||
raise RuntimeError("Invalid tweet URL: %s" % value)
|
raise RuntimeError("Invalid tweet URL: %s" % value)
|
||||||
candidate = match.group(1)
|
candidate = match.group(1)
|
||||||
@@ -656,6 +660,52 @@ def tweet(ctx, tweet_id, max_count, full_text, as_json, as_yaml):
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.argument("tweet_id")
|
||||||
|
@structured_output_options
|
||||||
|
@click.option("--markdown", "-m", "as_markdown", is_flag=True, help="Output article as Markdown.")
|
||||||
|
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save article Markdown to file.")
|
||||||
|
@click.pass_context
|
||||||
|
def article(ctx, tweet_id, as_json, as_yaml, as_markdown, output_file):
|
||||||
|
# type: (Any, str, bool, bool, bool, Optional[str]) -> None
|
||||||
|
"""Fetch a Twitter Article. TWEET_ID is the numeric tweet ID or full URL."""
|
||||||
|
compact = ctx.obj.get("compact", False)
|
||||||
|
if compact:
|
||||||
|
raise click.UsageError("`twitter article` does not support --compact. Use --markdown or --output.")
|
||||||
|
if as_markdown and (as_json or as_yaml):
|
||||||
|
raise click.UsageError("Use only one of --markdown, --json, or --yaml.")
|
||||||
|
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
client = _get_client_for_output(config, quiet=not rich_output)
|
||||||
|
if rich_output:
|
||||||
|
console.print("📰 Fetching article %s...\n" % tweet_id)
|
||||||
|
start = time.time()
|
||||||
|
article_tweet = client.fetch_article(tweet_id)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
if rich_output:
|
||||||
|
console.print("✅ Fetched article in %.1fs\n" % elapsed)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
_exit_with_error(exc)
|
||||||
|
|
||||||
|
markdown = article_to_markdown(article_tweet)
|
||||||
|
if output_file:
|
||||||
|
Path(output_file).write_text(markdown, encoding="utf-8")
|
||||||
|
if rich_output:
|
||||||
|
console.print("💾 Saved article Markdown 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):
|
||||||
|
return
|
||||||
|
|
||||||
|
print_article(article_tweet, console)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
@cli.command(name="list")
|
@cli.command(name="list")
|
||||||
@click.argument("list_id")
|
@click.argument("list_id")
|
||||||
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max tweets to fetch.")
|
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max tweets to fetch.")
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from .graphql import (
|
|||||||
_resolve_query_id,
|
_resolve_query_id,
|
||||||
_update_features_from_html,
|
_update_features_from_html,
|
||||||
)
|
)
|
||||||
from .models import UserProfile
|
from .models import Tweet, UserProfile
|
||||||
from .parser import (
|
from .parser import (
|
||||||
_deep_get,
|
_deep_get,
|
||||||
_parse_int,
|
_parse_int,
|
||||||
@@ -321,6 +321,45 @@ class TwitterClient:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def fetch_article(self, tweet_id):
|
||||||
|
# type: (str) -> Tweet
|
||||||
|
"""Fetch a Twitter Article by tweet ID."""
|
||||||
|
logger.debug("fetch_article: tweet_id=%s", tweet_id)
|
||||||
|
|
||||||
|
data = self._graphql_get(
|
||||||
|
"TweetResultByRestId",
|
||||||
|
variables={
|
||||||
|
"tweetId": tweet_id,
|
||||||
|
"withCommunity": False,
|
||||||
|
"includePromotedContent": False,
|
||||||
|
"withVoice": False,
|
||||||
|
},
|
||||||
|
features={
|
||||||
|
"longform_notetweets_consumption_enabled": True,
|
||||||
|
"responsive_web_twitter_article_tweet_consumption_enabled": True,
|
||||||
|
"longform_notetweets_rich_text_read_enabled": True,
|
||||||
|
"longform_notetweets_inline_media_enabled": True,
|
||||||
|
"articles_preview_enabled": True,
|
||||||
|
"responsive_web_graphql_exclude_directive_enabled": True,
|
||||||
|
"verified_phone_label_enabled": False,
|
||||||
|
},
|
||||||
|
field_toggles={
|
||||||
|
"withArticleRichContentState": True,
|
||||||
|
"withArticlePlainText": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _deep_get(data, "data", "tweetResult", "result")
|
||||||
|
if not result:
|
||||||
|
raise NotFoundError("Article not found: tweet_id=%s" % tweet_id)
|
||||||
|
|
||||||
|
tweet = parse_tweet_result(result)
|
||||||
|
if tweet is None or (tweet.article_title is None and tweet.article_text is None):
|
||||||
|
raise NotFoundError("Tweet %s has no article content" % tweet_id)
|
||||||
|
|
||||||
|
logger.info("fetch_article: tweet_id=%s", tweet_id)
|
||||||
|
return tweet
|
||||||
|
|
||||||
def fetch_list_timeline(self, list_id, count=20):
|
def fetch_list_timeline(self, list_id, count=20):
|
||||||
# type: (str, int) -> List[Tweet]
|
# type: (str, int) -> List[Tweet]
|
||||||
"""Fetch tweets from a Twitter List."""
|
"""Fetch tweets from a Twitter List."""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.markdown import Markdown
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
@@ -150,6 +151,61 @@ def print_tweet_detail(tweet: Tweet, console: Optional[Console] = None) -> None:
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def article_to_markdown(tweet: Tweet) -> str:
|
||||||
|
"""Convert a Twitter Article tweet into a Markdown document."""
|
||||||
|
title = tweet.article_title or "Twitter Article"
|
||||||
|
lines = [
|
||||||
|
"# %s" % title,
|
||||||
|
"",
|
||||||
|
"- Author: @%s (%s)" % (tweet.author.screen_name, tweet.author.name),
|
||||||
|
"- Published: %s" % (tweet.created_at or "unknown"),
|
||||||
|
"- URL: https://x.com/%s/status/%s" % (tweet.author.screen_name, tweet.id),
|
||||||
|
"- Likes: %s" % format_number(tweet.metrics.likes),
|
||||||
|
"- Retweets: %s" % format_number(tweet.metrics.retweets),
|
||||||
|
"- Replies: %s" % format_number(tweet.metrics.replies),
|
||||||
|
"- Bookmarks: %s" % format_number(tweet.metrics.bookmarks),
|
||||||
|
"- Views: %s" % format_number(tweet.metrics.views),
|
||||||
|
]
|
||||||
|
|
||||||
|
if tweet.article_text:
|
||||||
|
lines.extend(["", tweet.article_text.strip()])
|
||||||
|
|
||||||
|
return "\n".join(lines).strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def print_article(tweet: Tweet, console: Optional[Console] = None) -> None:
|
||||||
|
"""Print a Twitter Article with rich formatting."""
|
||||||
|
if console is None:
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
verified = " ✓" if tweet.author.verified else ""
|
||||||
|
title = tweet.article_title or "Twitter Article"
|
||||||
|
meta_parts = [
|
||||||
|
"By @%s%s (%s)" % (tweet.author.screen_name, verified, tweet.author.name),
|
||||||
|
"🕐 %s" % tweet.created_at,
|
||||||
|
"🔗 x.com/%s/status/%s" % (tweet.author.screen_name, tweet.id),
|
||||||
|
"",
|
||||||
|
"❤️ %s 🔄 %s 💬 %s 🔖 %s 👁️ %s"
|
||||||
|
% (
|
||||||
|
format_number(tweet.metrics.likes),
|
||||||
|
format_number(tweet.metrics.retweets),
|
||||||
|
format_number(tweet.metrics.replies),
|
||||||
|
format_number(tweet.metrics.bookmarks),
|
||||||
|
format_number(tweet.metrics.views),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
console.print(Panel(
|
||||||
|
"\n".join(meta_parts),
|
||||||
|
title="📰 %s" % title,
|
||||||
|
border_style="blue",
|
||||||
|
expand=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
if tweet.article_text:
|
||||||
|
console.print()
|
||||||
|
console.print(Markdown(tweet.article_text))
|
||||||
|
|
||||||
|
|
||||||
def print_filter_stats(
|
def print_filter_stats(
|
||||||
original_count: int,
|
original_count: int,
|
||||||
filtered: List[Tweet],
|
filtered: List[Tweet],
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ FALLBACK_QUERY_IDS = {
|
|||||||
"DeleteRetweet": "iQtK4dl5hBmXewYZuEOKVw",
|
"DeleteRetweet": "iQtK4dl5hBmXewYZuEOKVw",
|
||||||
"CreateBookmark": "aoDbu3RHznuiSkQ9aNM67Q",
|
"CreateBookmark": "aoDbu3RHznuiSkQ9aNM67Q",
|
||||||
"DeleteBookmark": "Wlmlj2-xISYCixDmuS8KNg",
|
"DeleteBookmark": "Wlmlj2-xISYCixDmuS8KNg",
|
||||||
|
"TweetResultByRestId": "7xflPyRiUxGVbJd4uWmbfg",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Default feature flags ────────────────────────────────────────────────
|
# ── Default feature flags ────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user