- Migrate user profile fields from `legacy{}` to `core{}` / `avatar{}` / `location{}` (with legacy fallback for older response shapes). Affects `user`, `whoami`, `status`, `followers`, `following`.
- Fix `user-posts` empty results: add missing `includePromotedContent` variable and update `instructions` path from `timeline_v2` to `timeline`.
- Switch `followers` / `following` to POST (Twitter changed the requirement).
- Refresh 16 stale `FALLBACK_QUERY_IDS` from fa0311/twitter-openapi.
- Drop `legacy{}` early-return in `parse_user_result`; key existence on `rest_id` so followers/following keep working when Twitter fully drops legacy.
- Add unit tests for the new core/avatar/location fallback chain and rest_id/typename guards.
Twitter migrated SearchTimeline from GET to POST; update fetch_search
to route through _graphql_post via a new use_post flag on _fetch_timeline,
avoiding duplicating the pagination logic (closes#39, refs #40, #42).
Also guard against get_ondemand_file_url returning None so the error
message is clear instead of crashing on NoneType.split (closes #43-adjacent).
Refresh all FALLBACK_QUERY_IDS from live JS bundles — 18 operations updated.
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>
- Fix auth.py subprocess script Windows Edge cookie path inconsistency
- Add timeutil.py for UTC→local time and relative time conversion
- Integrate time localization into formatter.py and serialization.py
- Add --output/-o option to show command for saving tweet detail as JSON
- Remove constants.py legacy aliases (USER_AGENT, SEC_CH_UA)
- Remove client.py backward-compat delegation methods and re-exports
- Update test imports to use parser module directly
- Add upload_media() method to TwitterClient (INIT/APPEND/FINALIZE flow
via upload.twitter.com, supports JPEG/PNG/GIF/WebP up to 5MB)
- Extend create_tweet() and quote_tweet() with optional media_ids param
- Add --image/-i option to post, reply, and quote CLI commands (max 4)
- Add MediaUploadError exception for upload-specific error handling
- Add 6 unit tests covering upload flow, validation, and media_ids
Co-Authored-By: Catafal <67582323+Catafal@users.noreply.github.com>
Changed the exception handling in _best_chrome_target() to catch
specific ImportError instead of broad Exception. This improves
code clarity and avoids masking unexpected errors.
Also added a debug log message to help diagnose when curl_cffi
is not available.
Co-authored-by: Security Bot <agent@example.com>
Twitter now returns HTTP 422 GRAPHQL_VALIDATION_FAILED (not just 404)
when a queryId goes stale. Updated fallback IDs and added 422 to the
stale-queryId retry logic in both _graphql_get and _graphql_post.
Twitter changed the response format from a list with nested 'user'
objects to {"users": [{user_id, name, screen_name, ...}]} with
minimal fields. Now extracts screen_name from the new format and
fetches the full profile via GraphQL UserByScreenName endpoint.
Split the monolithic client.py (1341 lines) into three focused modules:
- graphql.py (~200 lines): queryId resolution, URL building, JS bundle
scanning, feature flag management
- parser.py (~270 lines): Tweet/User/Media/Article parsing, utility functions
(_deep_get, _parse_int, _extract_cursor, _extract_media)
- client.py (~700 lines): TwitterClient class with HTTP engine, anti-detection,
session management, and all public API methods
Backward compatibility: client.py re-exports all previously public symbols
so existing test imports work without modification. 88/88 tests pass.
- Create exceptions.py with 7 exception types: TwitterError, AuthenticationError,
RateLimitError, NotFoundError, NetworkError, QueryIdError, TwitterAPIError
- Remove inline TwitterAPIError from client.py, import from exceptions module
- Replace RuntimeError('Cannot resolve queryId') with QueryIdError
- Replace RuntimeError('User not found') with NotFoundError
- Update test assertion for new TwitterAPIError message format
CLI-level smoke tests using --yaml output against real Twitter API.
Default skipped via @pytest.mark.smoke marker + pyproject.toml addopts.
Run locally with: uv run pytest -m smoke -v
- client.py:
- Remove dead _extract_cursor second branch (unreachable code)
- Cache SSL context as module-level _SSL_CTX (avoid re-reading CA certs)
- Add 404 stale-fallback retry to _graphql_post (parity with GET)
- Remove dead core.get('name')/core.get('screen_name') in fetch_user
- Set Content-Type: application/json only for POST requests
- Rename _to_int → _parse_int for clarity vs config._as_int
- Add 'not thread-safe' note on module-level caches
- cli.py:
- _fetch_and_display now accepts optional config param (fix double load)
- Refactor user_posts to use _fetch_and_display
- Pass config to all _fetch_and_display callers
- pyproject.toml:
- Move xclienttransaction/requests to optional [transaction] deps
- Add beautifulsoup4 to [transaction] optional deps
- README.md:
- Add rateLimit config section with comments
- Add constants.py to project structure tree
- _get_client: remove useless try/except that re-raised same error
- verify_cookies: increase timeout from 3s to 5s
- fetch_user: use _deep_get for URL extraction (consistent with
_parse_user_result)
- formatter: remove no-op tweets_to_json wrapper and unused import
- _as_int/_as_float: filter.py now imports from config.py (dedup)
- CLI read commands: extract _fetch_and_display() to dedup
favorite/search/likes/list_timeline
- _write_action: move load_config inside try block
- auth.py: add PEP 8 blank line after logger
Bug fixes:
- _extract_cursor: only extract Bottom cursors, preventing Top cursor
from corrupting pagination state
- _api_request: merge _api_get/_api_post into unified method — POST
now has rate-limit code 88 retry (was missing)
- fetch_user_likes: add override_base_variables=True
Code quality:
- Extract BEARER_TOKEN and USER_AGENT into constants.py (was duped
in auth.py and client.py)
- Add user_profile_to_dict/users_to_json for proper UserProfile
serialization (followers/following JSON output was ad-hoc)
- Refactor 6 CLI write commands via _write_action helper
- Extract _extract_media and _extract_author from _parse_tweet_result
- Update CLI module docstring with all 18 commands
- TweetDetail requires fieldToggles (withArticleRichContentState: true)
to populate tweet_results in entries — without it, server returns {}
- Add fieldToggles parameter throughout: _build_graphql_url, _graphql_get,
_fetch_timeline
- Tested: tweet detail, followers, following, like — all working
- Add 'twitter search' command with --type (Top/Latest/Photos/Videos), --max, --json, --filter
- Add 'twitter likes' command to view tweets liked by a user
- Add SearchTimeline and Likes GraphQL operations with fallback queryIds
- Update README with new command examples (EN + CN)