feat: add bookmark folders support (#30)
Add `twitter bookmarks folders` command to list bookmark folders and `twitter bookmarks folders <id>` to fetch tweets from a specific folder, with --since date filtering and pagination support. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ Read commands:
|
||||
twitter feed # home timeline (For You)
|
||||
twitter feed -t following # following feed
|
||||
twitter bookmarks # bookmarks
|
||||
twitter bookmarks folders # list bookmark folders
|
||||
twitter bookmarks folders <id> # tweets in a folder
|
||||
twitter search "query" # search tweets
|
||||
twitter search "query" --from user # advanced search
|
||||
twitter user elonmusk # user profile
|
||||
@@ -458,7 +460,7 @@ def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter, full_tex
|
||||
)
|
||||
|
||||
|
||||
@cli.command(name="bookmarks")
|
||||
@cli.group(name="bookmarks", invoke_without_command=True)
|
||||
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max number of tweets to fetch.")
|
||||
@structured_output_options
|
||||
@click.option("--output", "-o", "output_file", type=str, default=None, help="Save tweets to JSON file.")
|
||||
@@ -467,16 +469,141 @@ def favorites(ctx, max_count, as_json, as_yaml, output_file, do_filter, full_tex
|
||||
@click.pass_context
|
||||
def bookmarks(ctx, max_count, as_json, as_yaml, output_file, do_filter, full_text):
|
||||
# type: (Any, Optional[int], bool, bool, Optional[str], bool, bool) -> None
|
||||
"""Fetch bookmarked tweets."""
|
||||
_run_bookmarks_command(
|
||||
max_count,
|
||||
as_json,
|
||||
as_yaml,
|
||||
output_file,
|
||||
do_filter,
|
||||
compact=ctx.obj.get("compact", False),
|
||||
full_text=full_text,
|
||||
)
|
||||
"""Fetch bookmarked tweets, or manage bookmark folders."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
_run_bookmarks_command(
|
||||
max_count,
|
||||
as_json,
|
||||
as_yaml,
|
||||
output_file,
|
||||
do_filter,
|
||||
compact=ctx.obj.get("compact", False),
|
||||
full_text=full_text,
|
||||
)
|
||||
|
||||
|
||||
@bookmarks.command(name="folders")
|
||||
@click.argument("folder_id", required=False, default=None)
|
||||
@click.option("--max", "-n", "max_count", type=int, default=None, help="Max tweets to fetch from folder.")
|
||||
@click.option("--since", type=str, default=None, help="Only show tweets after this date (YYYY-MM-DD).")
|
||||
@structured_output_options
|
||||
@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.pass_context
|
||||
def bookmarks_folders(ctx, folder_id, max_count, since, as_json, as_yaml, output_file, do_filter):
|
||||
# type: (Any, Optional[str], Optional[int], Optional[str], bool, bool, Optional[str], bool) -> None
|
||||
"""List bookmark folders, or fetch tweets from a folder.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
twitter bookmarks folders # list all folders
|
||||
twitter bookmarks folders <id> # tweets in folder
|
||||
twitter bookmarks folders <id> -n 50 # max 50 tweets
|
||||
twitter bookmarks folders <id> --since 2026-01-01
|
||||
"""
|
||||
compact = ctx.obj.get("compact", False)
|
||||
|
||||
if folder_id is None:
|
||||
_run_list_bookmark_folders(as_json, as_yaml, compact)
|
||||
else:
|
||||
_run_bookmark_folder_timeline(
|
||||
folder_id, max_count, since, as_json, as_yaml, output_file, do_filter, compact,
|
||||
)
|
||||
|
||||
|
||||
def _run_list_bookmark_folders(as_json, as_yaml, compact):
|
||||
# type: (bool, bool, bool) -> None
|
||||
config = load_config()
|
||||
rich_output = use_rich_output(as_json=as_json, as_yaml=as_yaml, compact=compact)
|
||||
|
||||
def _run():
|
||||
client = _get_client(config)
|
||||
if rich_output:
|
||||
console.print("\U0001f4c2 Fetching bookmark folders...\n")
|
||||
folders = client.fetch_bookmark_folders()
|
||||
if rich_output:
|
||||
console.print("\u2705 Found %d bookmark folders\n" % len(folders))
|
||||
|
||||
from .serialization import bookmark_folders_to_data
|
||||
data = bookmark_folders_to_data(folders)
|
||||
|
||||
if compact:
|
||||
import json as _json
|
||||
click.echo(_json.dumps(data, ensure_ascii=False, indent=2))
|
||||
return
|
||||
|
||||
if emit_structured(data, as_json=as_json, as_yaml=as_yaml):
|
||||
return
|
||||
|
||||
# Rich table output
|
||||
from rich.table import Table
|
||||
table = Table(title="\U0001f4c2 Bookmark Folders \u2014 %d folders" % len(folders))
|
||||
table.add_column("ID", style="dim")
|
||||
table.add_column("Name", style="bold")
|
||||
for folder in folders:
|
||||
table.add_row(folder.id, folder.name)
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
_run_guarded(_run)
|
||||
|
||||
|
||||
def _parse_since_date(since_str):
|
||||
# type: (str) -> Any
|
||||
"""Parse a YYYY-MM-DD date string into a datetime for filtering."""
|
||||
from datetime import datetime, timezone
|
||||
try:
|
||||
return datetime.strptime(since_str, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
raise RuntimeError("Invalid --since date format. Use YYYY-MM-DD (e.g. 2026-01-15).")
|
||||
|
||||
|
||||
def _filter_tweets_since(tweets, since_str):
|
||||
# type: (List[Tweet], str) -> List[Tweet]
|
||||
"""Filter tweets to only those created after the given date."""
|
||||
from datetime import datetime, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
cutoff = _parse_since_date(since_str)
|
||||
filtered = []
|
||||
for tweet in tweets:
|
||||
if not tweet.created_at:
|
||||
continue
|
||||
try:
|
||||
tweet_dt = parsedate_to_datetime(tweet.created_at)
|
||||
if tweet_dt >= cutoff:
|
||||
filtered.append(tweet)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
return filtered
|
||||
|
||||
|
||||
def _run_bookmark_folder_timeline(folder_id, max_count, since, as_json, as_yaml, output_file, do_filter, compact):
|
||||
# type: (str, Optional[int], Optional[str], bool, bool, Optional[str], bool, bool) -> None
|
||||
config = load_config()
|
||||
|
||||
def _run():
|
||||
client = _get_client(config)
|
||||
|
||||
def fetch_fn(count):
|
||||
tweets = client.fetch_bookmark_folder_timeline(folder_id, count)
|
||||
if since:
|
||||
tweets = _filter_tweets_since(tweets, since)
|
||||
return tweets
|
||||
|
||||
_fetch_and_display(
|
||||
fetch_fn,
|
||||
"bookmark folder %s" % folder_id,
|
||||
"\U0001f4c2",
|
||||
max_count,
|
||||
as_json,
|
||||
as_yaml,
|
||||
output_file,
|
||||
do_filter,
|
||||
config,
|
||||
compact=compact,
|
||||
)
|
||||
|
||||
_run_guarded(_run)
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
||||
Reference in New Issue
Block a user