feat: add full-text option for tweet tables

This commit is contained in:
jackwener
2026-03-11 20:58:12 +08:00
parent 88a9f4ce97
commit 1313eb0be1
5 changed files with 145 additions and 32 deletions

View File

@@ -26,6 +26,7 @@ A terminal-first CLI for Twitter/X: read timelines, bookmarks, and user profiles
- Tweet detail: view a tweet and its replies - Tweet detail: view a tweet and its replies
- 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
- Structured output: export any data as YAML or JSON for scripting and AI agent integration - Structured output: export any data as YAML or JSON for scripting and AI agent integration
- Optional scoring filter: rank tweets by engagement weights - Optional scoring filter: rank tweets by engagement weights
- Structured output contract: [SCHEMA.md](./SCHEMA.md) - Structured output contract: [SCHEMA.md](./SCHEMA.md)
@@ -94,32 +95,39 @@ twitter feed --filter
```bash ```bash
# Feed # Feed
twitter feed --max 50 twitter feed --max 50
twitter feed --full-text
twitter feed --json > tweets.json twitter feed --json > tweets.json
twitter feed --input tweets.json twitter feed --input tweets.json
# Bookmarks # Bookmarks
twitter bookmarks twitter bookmarks
twitter bookmarks --full-text
twitter bookmarks --max 30 --yaml twitter bookmarks --max 30 --yaml
# Search # Search
twitter search "Claude Code" twitter search "Claude Code"
twitter search "AI agent" -t Latest --max 50 twitter search "AI agent" -t Latest --max 50
twitter search "AI agent" --full-text
twitter search "机器学习" --yaml twitter search "机器学习" --yaml
twitter search "topic" -o results.json # Save to file twitter search "topic" -o results.json # Save to file
twitter search "trending" --filter # Apply ranking filter twitter search "trending" --filter # Apply ranking filter
# Tweet detail (view tweet + replies) # Tweet detail (view tweet + replies)
twitter tweet 1234567890 twitter tweet 1234567890
twitter tweet 1234567890 --full-text
twitter tweet https://x.com/user/status/1234567890 twitter tweet https://x.com/user/status/1234567890
# List timeline # List timeline
twitter list 1539453138322673664 twitter list 1539453138322673664
twitter list 1539453138322673664 --full-text
# User # User
twitter user elonmusk twitter user elonmusk
twitter user-posts elonmusk --max 20 twitter user-posts elonmusk --max 20
twitter user-posts elonmusk --full-text
twitter user-posts elonmusk -o tweets.json twitter user-posts elonmusk -o tweets.json
twitter likes elonmusk --max 30 # ⚠️ own likes only (private since Jun 2024) twitter likes elonmusk --max 30 # ⚠️ own likes only (private since Jun 2024)
twitter likes elonmusk --full-text
twitter likes elonmusk -o likes.json twitter likes elonmusk -o likes.json
twitter followers elonmusk --max 50 twitter followers elonmusk --max 50
twitter following elonmusk --max 50 twitter following elonmusk --max 50
@@ -201,6 +209,7 @@ rateLimit:
Fetch behavior: Fetch behavior:
- `fetch.count` is the default item count for read commands when `--max` is omitted - `fetch.count` is the default item count for read commands when `--max` is omitted
- Rich table output truncates long tweet text by default; use `--full-text` to show full body text in list views
Filter behavior: Filter behavior:
@@ -231,6 +240,13 @@ Mode behavior:
- **Use browser cookie extraction** — provides full cookie fingerprint - **Use browser cookie extraction** — provides full cookie fingerprint
- **Avoid datacenter IPs** — residential proxies are much safer - **Avoid datacenter IPs** — residential proxies are much safer
### Output Modes
- Use the default rich table for interactive reading
- Use `--full-text` when reading long posts in terminal tables
- Use `--yaml` or `--json` for scripts and agent pipelines
- Use `-c` / `--compact` when token efficiency matters more than completeness
### Troubleshooting ### Troubleshooting
- `No Twitter cookies found` - `No Twitter cookies found`
@@ -330,6 +346,7 @@ After installation, OpenClaw can call `twitter-cli` commands directly.
- 推文详情:查看推文及其回复 - 推文详情:查看推文及其回复
- 列表时间线:获取 Twitter List 的推文 - 列表时间线:获取 Twitter List 的推文
- 用户查询:查看用户资料、推文、点赞、粉丝和关注 - 用户查询:查看用户资料、推文、点赞、粉丝和关注
- `--full-text`:在 rich table 输出里关闭推文正文截断
- 结构化输出:支持 YAML 和 JSON便于脚本处理和 AI agent 集成 - 结构化输出:支持 YAML 和 JSON便于脚本处理和 AI agent 集成
> **AI Agent 提示:** 需要结构化输出时优先使用 `--yaml`,除非下游必须是 JSON。stdout 不是 TTY 时默认输出 YAML。用 `--max` 控制返回数量。 > **AI Agent 提示:** 需要结构化输出时优先使用 `--yaml`,除非下游必须是 JSON。stdout 不是 TTY 时默认输出 YAML。用 `--max` 控制返回数量。
@@ -374,27 +391,34 @@ uv tool upgrade twitter-cli
twitter feed twitter feed
twitter feed -t following twitter feed -t following
twitter feed --filter twitter feed --filter
twitter feed --full-text
# 收藏 # 收藏
twitter bookmarks twitter bookmarks
twitter bookmarks --full-text
# 搜索 # 搜索
twitter search "Claude Code" twitter search "Claude Code"
twitter search "AI agent" -t Latest --max 50 twitter search "AI agent" -t Latest --max 50
twitter search "AI agent" --full-text
twitter search "topic" -o results.json # 保存到文件 twitter search "topic" -o results.json # 保存到文件
twitter search "trending" --filter # 启用排序筛选 twitter search "trending" --filter # 启用排序筛选
# 推文详情 # 推文详情
twitter tweet 1234567890 twitter tweet 1234567890
twitter tweet 1234567890 --full-text
# 列表时间线 # 列表时间线
twitter list 1539453138322673664 twitter list 1539453138322673664
twitter list 1539453138322673664 --full-text
# 用户 # 用户
twitter user elonmusk twitter user elonmusk
twitter user-posts elonmusk --max 20 twitter user-posts elonmusk --max 20
twitter user-posts elonmusk --full-text
twitter user-posts elonmusk -o tweets.json twitter user-posts elonmusk -o tweets.json
twitter likes elonmusk --max 30 # ⚠️ 仅可查看自己的点赞2024年6月起平台已私密化 twitter likes elonmusk --max 30 # ⚠️ 仅可查看自己的点赞2024年6月起平台已私密化
twitter likes elonmusk --full-text
twitter likes elonmusk -o likes.json twitter likes elonmusk -o likes.json
twitter followers elonmusk twitter followers elonmusk
twitter following elonmusk twitter following elonmusk
@@ -445,6 +469,8 @@ export TWITTER_PROXY=socks5://127.0.0.1:1080
未传 `--max` 时,所有读取命令默认使用 `config.yaml` 里的 `fetch.count`。 未传 `--max` 时,所有读取命令默认使用 `config.yaml` 里的 `fetch.count`。
rich table 输出默认会截断较长正文;如果需要在列表视图中查看完整正文,可加 `--full-text`。
只有在传入 `--filter` 时才会启用筛选评分;默认不筛选。 只有在传入 `--filter` 时才会启用筛选评分;默认不筛选。
评分公式: 评分公式:
@@ -490,6 +516,13 @@ score = likes_w * likes
- **避免数据中心 IP** — 住宅代理更安全 - **避免数据中心 IP** — 住宅代理更安全
- Cookie 仅在本地使用,不会被本工具上传 - Cookie 仅在本地使用,不会被本工具上传
### 输出模式建议
- 默认 rich table 适合终端交互式浏览
- 需要在表格里看完整正文时,使用 `--full-text`
- 需要脚本消费时,优先使用 `--yaml` 或 `--json`
- 需要节省 token 时,使用 `-c` / `--compact`
### 作为 AI Agent Skill 使用 ### 作为 AI Agent Skill 使用
twitter-cli 提供了 [`SKILL.md`](./SKILL.md),可让 AI Agent 更稳定地调用本工具。 twitter-cli 提供了 [`SKILL.md`](./SKILL.md),可让 AI Agent 更稳定地调用本工具。

View File

@@ -114,6 +114,19 @@ twitter feed --json | jq '.[0].text'
All machine-readable output uses the envelope documented in [SCHEMA.md](./SCHEMA.md). All machine-readable output uses the envelope documented in [SCHEMA.md](./SCHEMA.md).
Tweet and user payloads now live under `.data`. Tweet and user payloads now live under `.data`.
### Full text: `--full-text` flag (rich tables only)
Use `--full-text` when the user wants complete post bodies in terminal tables.
It affects rich table list views such as `feed`, `bookmarks`, `search`, `user-posts`, `likes`, `list`, and reply tables in `tweet`.
It does **not** change `--json`, `--yaml`, or `-c` compact output.
```bash
twitter feed --full-text
twitter search "AI agent" --full-text
twitter user-posts elonmusk --max 20 --full-text
twitter tweet 1234567890 --full-text
```
### Compact: `-c` flag (minimal tokens for LLM) ### Compact: `-c` flag (minimal tokens for LLM)
```bash ```bash
@@ -138,19 +151,26 @@ twitter user elonmusk --json # JSON output
twitter feed # Home timeline (For You) twitter feed # Home timeline (For You)
twitter feed -t following # Following timeline twitter feed -t following # Following timeline
twitter feed --max 50 # Limit count twitter feed --max 50 # Limit count
twitter feed --full-text # Show full post body in table
twitter feed --filter # Enable ranking filter twitter feed --filter # Enable ranking filter
twitter feed --yaml > tweets.yaml # Export as YAML twitter feed --yaml > tweets.yaml # Export as YAML
twitter feed --input tweets.json # Read from local JSON file twitter feed --input tweets.json # Read from local JSON file
twitter bookmarks # Bookmarked tweets twitter bookmarks # Bookmarked tweets
twitter bookmarks --full-text # Full text in bookmarks table
twitter bookmarks --max 30 --yaml twitter bookmarks --max 30 --yaml
twitter search "keyword" # Search tweets twitter search "keyword" # Search tweets
twitter search "AI agent" -t Latest --max 50 twitter search "AI agent" -t Latest --max 50
twitter search "AI agent" --full-text # Full text in search results
twitter search "topic" -o results.json # Save to file twitter search "topic" -o results.json # Save to file
twitter tweet 1234567890 # Tweet detail + replies twitter tweet 1234567890 # Tweet detail + replies
twitter tweet 1234567890 --full-text # Full text in reply table
twitter tweet https://x.com/user/status/12345 # Accepts URL twitter tweet https://x.com/user/status/12345 # Accepts URL
twitter list 1539453138322673664 # List timeline twitter list 1539453138322673664 # List timeline
twitter list 1539453138322673664 --full-text
twitter user-posts elonmusk --max 20 # User's tweets twitter user-posts elonmusk --max 20 # User's tweets
twitter user-posts elonmusk --full-text
twitter likes elonmusk --max 30 # User's likes (own only, see note) twitter likes elonmusk --max 30 # User's likes (own only, see note)
twitter likes elonmusk --full-text
twitter followers elonmusk --max 50 # Followers twitter followers elonmusk --max 50 # Followers
twitter following elonmusk --max 50 # Following twitter following elonmusk --max 50 # Following
``` ```
@@ -241,6 +261,10 @@ twitter followers "$MY_NAME" --max 200 --json | jq -r '.data[].username' | grep
twitter -c feed -t following --max 30 twitter -c feed -t following --max 30
twitter -c bookmarks --max 20 twitter -c bookmarks --max 20
# Rich table with complete post bodies
twitter feed -t following --max 20 --full-text
twitter search "AI agent" --max 20 --full-text
# Full JSON for analysis # Full JSON for analysis
twitter feed -t following --max 30 -o following.json twitter feed -t following --max 30 -o following.json
twitter bookmarks --max 20 -o bookmarks.json twitter bookmarks --max 20 -o bookmarks.json

View File

@@ -2,9 +2,11 @@ from __future__ import annotations
from click.testing import CliRunner from click.testing import CliRunner
import pytest import pytest
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.models import UserProfile from twitter_cli.models import UserProfile
from twitter_cli.serialization import tweets_to_json from twitter_cli.serialization import tweets_to_json
@@ -42,6 +44,27 @@ def test_cli_feed_json_input_path(tmp_path, tweet_factory) -> None:
assert '"id": "1"' in result.output assert '"id": "1"' in result.output
def test_print_tweet_table_truncates_text_by_default(tweet_factory) -> None:
long_text = "A" * 140
console = Console(record=True, width=400)
print_tweet_table([tweet_factory("1", text=long_text)], console=console)
output = console.export_text()
assert ("A" * 117 + "...") in output
def test_print_tweet_table_full_text_shows_untruncated_text(tweet_factory) -> None:
long_text = "B" * 140
console = Console(record=True, width=400)
print_tweet_table([tweet_factory("1", text=long_text)], console=console, full_text=True)
output = console.export_text()
assert long_text in output
assert ("B" * 117 + "...") not in output
@pytest.mark.parametrize( @pytest.mark.parametrize(
"args", "args",
[ [

View File

@@ -311,8 +311,8 @@ def cli(ctx, verbose, compact):
ctx.obj["compact"] = compact ctx.obj["compact"] = compact
def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, as_yaml, output_file, do_filter, config=None, compact=False): def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, as_yaml, output_file, do_filter, config=None, compact=False, full_text=False):
# type: (Any, str, str, Optional[int], bool, bool, Optional[str], bool, Optional[dict], bool) -> None # type: (Any, str, str, Optional[int], bool, bool, Optional[str], bool, Optional[dict], bool, bool) -> None
"""Common fetch-filter-display logic for timeline-like commands.""" """Common fetch-filter-display logic for timeline-like commands."""
if config is None: if config is None:
config = load_config() config = load_config()
@@ -343,12 +343,17 @@ def _fetch_and_display(fetch_fn, label, emoji, max_count, as_json, as_yaml, outp
if emit_structured(tweets_to_data(filtered), as_json=as_json, as_yaml=as_yaml): if emit_structured(tweets_to_data(filtered), as_json=as_json, as_yaml=as_yaml):
return return
print_tweet_table(filtered, console, title="%s %s%d tweets" % (emoji, label, len(filtered))) print_tweet_table(
filtered,
console,
title="%s %s%d tweets" % (emoji, label, len(filtered)),
full_text=full_text,
)
console.print() console.print()
def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=False): def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=False, full_text=False):
# type: (Optional[int], bool, bool, Optional[str], bool, bool) -> None # type: (Optional[int], bool, bool, Optional[str], bool, bool, bool) -> None
config = load_config() config = load_config()
def _run(): def _run():
@@ -364,6 +369,7 @@ def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter,
do_filter, do_filter,
config, config,
compact=compact, compact=compact,
full_text=full_text,
) )
_run_guarded(_run) _run_guarded(_run)
@@ -383,9 +389,10 @@ def _run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter,
@click.option("--input", "-i", "input_file", type=str, default=None, help="Load tweets from JSON file.") @click.option("--input", "-i", "input_file", type=str, default=None, help="Load tweets from JSON file.")
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save filtered tweets to JSON file.") @click.option("--output", "-o", "output_file", type=str, default=None, help="Save filtered tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.") @click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context @click.pass_context
def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, do_filter): def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, do_filter, full_text):
# type: (Any, str, Optional[int], bool, bool, Optional[str], Optional[str], bool) -> None # type: (Any, str, Optional[int], bool, bool, Optional[str], Optional[str], bool, bool) -> None
"""Fetch home timeline with optional filtering.""" """Fetch home timeline with optional filtering."""
compact = ctx.obj.get("compact", False) compact = ctx.obj.get("compact", False)
rich_output = use_rich_output(as_json=as_json, as_yaml=as_yaml, compact=compact) rich_output = use_rich_output(as_json=as_json, as_yaml=as_yaml, compact=compact)
@@ -430,7 +437,7 @@ def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, d
title = "👥 Following" if feed_type == "following" else "📱 Twitter" title = "👥 Following" if feed_type == "following" else "📱 Twitter"
title += "%d tweets" % len(filtered) title += "%d tweets" % len(filtered)
print_tweet_table(filtered, console, title=title) print_tweet_table(filtered, console, title=title, full_text=full_text)
console.print() console.print()
@@ -439,11 +446,20 @@ def feed(ctx, feed_type, max_count, as_json, as_yaml, input_file, output_file, d
@structured_output_options @structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.") @click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.") @click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context @click.pass_context
def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter): def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, Optional[int], bool, bool, Optional[str], bool) -> None # type: (Any, Optional[int], bool, bool, Optional[str], bool, bool) -> None
"""Fetch bookmarked (favorite) tweets.""" """Fetch bookmarked (favorite) tweets."""
_run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=ctx.obj.get("compact", False)) _run_bookmarks_command(
max_count,
as_json,
as_yaml,
output_file,
do_filter,
compact=ctx.obj.get("compact", False),
full_text=full_text,
)
@cli.command(name="bookmarks") @cli.command(name="bookmarks")
@@ -451,11 +467,20 @@ def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter):
@structured_output_options @structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.") @click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.") @click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context @click.pass_context
def bookmarks(ctx, max_count, as_json, as_yaml, output_file, do_filter): def bookmarks(ctx, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, Optional[int], bool, bool, Optional[str], bool) -> None # type: (Any, Optional[int], bool, bool, Optional[str], bool, bool) -> None
"""Fetch bookmarked tweets.""" """Fetch bookmarked tweets."""
_run_bookmarks_command(max_count, as_json, as_yaml, output_file, do_filter, compact=ctx.obj.get("compact", False)) _run_bookmarks_command(
max_count,
as_json,
as_yaml,
output_file,
do_filter,
compact=ctx.obj.get("compact", False),
full_text=full_text,
)
@cli.command() @cli.command()
@@ -485,9 +510,10 @@ def user(screen_name, as_json, as_yaml):
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.") @click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.")
@structured_output_options @structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.") @click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context @click.pass_context
def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file): def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file, full_text):
# type: (Any, str, int, bool, bool, Optional[str]) -> None # type: (Any, str, int, bool, bool, Optional[str], bool) -> None
"""List a user's tweets. SCREEN_NAME is the @handle (without @).""" """List a user's tweets. SCREEN_NAME is the @handle (without @)."""
screen_name = screen_name.lstrip("@") screen_name = screen_name.lstrip("@")
compact = ctx.obj.get("compact", False) compact = ctx.obj.get("compact", False)
@@ -501,7 +527,7 @@ def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file):
_fetch_and_display( _fetch_and_display(
lambda count: client.fetch_user_tweets(profile.id, count), lambda count: client.fetch_user_tweets(profile.id, count),
"@%s tweets" % screen_name, "📝", max_count, as_json, as_yaml, output_file, False, config, "@%s tweets" % screen_name, "📝", max_count, as_json, as_yaml, output_file, False, config,
compact=compact, compact=compact, full_text=full_text,
) )
_run_guarded(_run) _run_guarded(_run)
@@ -520,9 +546,10 @@ def user_posts(ctx, screen_name, max_count, as_json, as_yaml, output_file):
@structured_output_options @structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.") @click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.") @click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context @click.pass_context
def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_filter): def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, str, str, int, bool, bool, Optional[str], bool) -> None # type: (Any, str, str, int, bool, bool, Optional[str], bool, bool) -> None
"""Search tweets by QUERY string.""" """Search tweets by QUERY string."""
compact = ctx.obj.get("compact", False) compact = ctx.obj.get("compact", False)
config = load_config() config = load_config()
@@ -532,7 +559,7 @@ def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_fil
_fetch_and_display( _fetch_and_display(
lambda count: client.fetch_search(query, count, product), lambda count: client.fetch_search(query, count, product),
"'%s' (%s)" % (query, product), "🔍", max_count, as_json, as_yaml, output_file, do_filter, config, "'%s' (%s)" % (query, product), "🔍", max_count, as_json, as_yaml, output_file, do_filter, config,
compact=compact, compact=compact, full_text=full_text,
) )
_run_guarded(_run) _run_guarded(_run)
@@ -543,9 +570,10 @@ def search(ctx, query, product, max_count, as_json, as_yaml, output_file, do_fil
@structured_output_options @structured_output_options
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.") @click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.") @click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context @click.pass_context
def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter): def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter, full_text):
# type: (Any, str, int, bool, bool, Optional[str], bool) -> None # type: (Any, str, int, bool, bool, Optional[str], bool, bool) -> None
"""Show tweets liked by a user. SCREEN_NAME is the @handle (without @). """Show tweets liked by a user. SCREEN_NAME is the @handle (without @).
NOTE: Twitter/X made all likes private since June 2024. You can only view NOTE: Twitter/X made all likes private since June 2024. You can only view
@@ -583,7 +611,7 @@ def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter)
_fetch_and_display( _fetch_and_display(
lambda count: client.fetch_user_likes(profile.id, count), lambda count: client.fetch_user_likes(profile.id, count),
"@%s likes" % screen_name, "❤️", max_count, as_json, as_yaml, output_file, do_filter, config, "@%s likes" % screen_name, "❤️", max_count, as_json, as_yaml, output_file, do_filter, config,
compact=compact, compact=compact, full_text=full_text,
) )
_run_guarded(_run) _run_guarded(_run)
@@ -591,10 +619,11 @@ def likes(ctx, screen_name, max_count, as_json, as_yaml, output_file, do_filter)
@cli.command() @cli.command()
@click.argument("tweet_id") @click.argument("tweet_id")
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max replies to fetch.") @click.option("--max", "-n", "max_count", type=int, default=None, help="Max replies to fetch.")
@click.option("--full-text", is_flag=True, help="Show full reply text in table output.")
@structured_output_options @structured_output_options
@click.pass_context @click.pass_context
def tweet(ctx, tweet_id, max_count, as_json, as_yaml): def tweet(ctx, tweet_id, max_count, full_text, as_json, as_yaml):
# type: (Any, str, int, bool, bool) -> None # type: (Any, str, int, bool, bool, bool) -> None
"""View a tweet and its replies. TWEET_ID is the numeric tweet ID or full URL.""" """View a tweet and its replies. TWEET_ID is the numeric tweet ID or full URL."""
compact = ctx.obj.get("compact", False) compact = ctx.obj.get("compact", False)
tweet_id = _normalize_tweet_id(tweet_id) tweet_id = _normalize_tweet_id(tweet_id)
@@ -623,7 +652,7 @@ def tweet(ctx, tweet_id, max_count, as_json, as_yaml):
print_tweet_detail(tweets[0], console) print_tweet_detail(tweets[0], console)
if len(tweets) > 1: if len(tweets) > 1:
console.print("\n💬 Replies:") console.print("\n💬 Replies:")
print_tweet_table(tweets[1:], console, title="💬 Replies — %d" % (len(tweets) - 1)) print_tweet_table(tweets[1:], console, title="💬 Replies — %d" % (len(tweets) - 1), full_text=full_text)
console.print() console.print()
@@ -632,9 +661,10 @@ def tweet(ctx, tweet_id, max_count, as_json, as_yaml):
@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.")
@structured_output_options @structured_output_options
@click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.") @click.option("--filter", "do_filter", is_flag=True, help="Enable score-based filtering.")
@click.option("--full-text", is_flag=True, help="Show full tweet text in table output.")
@click.pass_context @click.pass_context
def list_timeline(ctx, list_id, max_count, as_json, as_yaml, do_filter): def list_timeline(ctx, list_id, max_count, as_json, as_yaml, do_filter, full_text):
# type: (Any, str, int, bool, bool, bool) -> None # type: (Any, str, int, bool, bool, bool, bool) -> None
"""Fetch tweets from a Twitter List. LIST_ID is the numeric list ID.""" """Fetch tweets from a Twitter List. LIST_ID is the numeric list ID."""
compact = ctx.obj.get("compact", False) compact = ctx.obj.get("compact", False)
config = load_config() config = load_config()
@@ -643,7 +673,7 @@ def list_timeline(ctx, list_id, max_count, as_json, as_yaml, do_filter):
_fetch_and_display( _fetch_and_display(
lambda count: client.fetch_list_timeline(list_id, count), lambda count: client.fetch_list_timeline(list_id, count),
"list %s" % list_id, "📋", max_count, as_json, as_yaml, None, do_filter, config, "list %s" % list_id, "📋", max_count, as_json, as_yaml, None, do_filter, config,
compact=compact, compact=compact, full_text=full_text,
) )
_run_guarded(_run) _run_guarded(_run)

View File

@@ -24,6 +24,7 @@ def print_tweet_table(
tweets: List[Tweet], tweets: List[Tweet],
console: Optional[Console] = None, console: Optional[Console] = None,
title: Optional[str] = None, title: Optional[str] = None,
full_text: bool = False,
) -> None: ) -> None:
"""Print tweets as a rich table.""" """Print tweets as a rich table."""
if console is None: if console is None:
@@ -46,9 +47,9 @@ def print_tweet_table(
if tweet.is_retweet and tweet.retweeted_by: if tweet.is_retweet and tweet.retweeted_by:
author_text += "\n🔄 @%s" % tweet.retweeted_by author_text += "\n🔄 @%s" % tweet.retweeted_by
# Tweet text (truncated) # Tweet text
text = tweet.text.replace("\n", " ").strip() text = tweet.text.replace("\n", " ").strip()
if len(text) > 120: if not full_text and len(text) > 120:
text = text[:117] + "..." text = text[:117] + "..."
# Media indicators # Media indicators
@@ -66,7 +67,9 @@ def print_tweet_table(
# Quoted tweet # Quoted tweet
if tweet.quoted_tweet: if tweet.quoted_tweet:
qt = tweet.quoted_tweet qt = tweet.quoted_tweet
qt_text = qt.text.replace("\n", " ")[:60] qt_text = qt.text.replace("\n", " ")
if not full_text and len(qt_text) > 60:
qt_text = qt_text[:57] + "..."
text += "\n┌ @%s: %s" % (qt.author.screen_name, qt_text) text += "\n┌ @%s: %s" % (qt.author.screen_name, qt_text)
# Tweet link # Tweet link