feat: add full-text option for tweet tables
This commit is contained in:
33
README.md
33
README.md
@@ -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 更稳定地调用本工具。
|
||||||
|
|||||||
24
SKILL.md
24
SKILL.md
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user