- Read each operation's current query id and feature flags from X's web
client, keep them in graphql-ops.json, refresh them hourly, and read them
again when two accounts get nothing back for the same call.
- Leave an account out of an operation for a day when X answers it with an
empty 404 while another account gets results. Check unchecked accounts'
search access in the background and prefer accounts known to work.
- Send search the way Chrome does (variables, features, referer) and load
X's script chunks with Chrome's headers. Keep a transaction-id refusal only
when dropping the id helped.
- Require three handshakes before using a browser version; one lucky
handshake let Chrome 152 through while X refused it.
- Add a round-robin fallback proxy pool for HTML 429s, with clients shared
per browser so exits reuse connections.
- Drop the browser auto-retry and the per-account retries on empty answers,
and log expected empty answers at trace.
The hand-rolled hyper/rustls transport advertised a rustls ClientHello with
http/1.1 only, a fingerprint no browser has. Requests now go through primp:
each session gets its own client with a browser's TLS and HTTP/2 fingerprint
and matching headers, drawn from the session id so an account keeps its
browser across restarts like its proxy port. The mix is weighted like desktop
traffic and the OS always fits the engine. Each engine uses the newest version
X's edge accepts, found by a handshake at startup, since Chrome 152 is refused.
Sessions multiplex one pinged HTTP/2 connection each and resume TLS with PSK.
Also: parse each GraphQL body once instead of twice; cut the photo rail from
the cached media page (and file a fetched one) so a profile view and the media
tab share one UserMedia call; skip the duplicate TweetDetail on article posts;
probe SOCKS exits concurrently (startup 12s to 2.5s); stop taking a write lock
per request in the transaction-id refusal check and refresh the id client in
the background.
Cookie sessions now get a private adult birthdate written once so media stops rendering as the birthdate interstitial. Search drops the broken Lists tab and the timeline/media UI stays consistent with the rest of the layout.
New accounts hide sensitive media and strip it from search, which leaks into teapawt results; each cookie session now POSTs those settings off once at startup.
Shows a 'Translate post' button on tweets where legacy.lang is not
English. Clicking it fires an htmx GET to /translate/{id} which calls
Twitter's Strato translation API (Google Translate backend) and swaps
the button with the translated text and source language header. Adds
lang and is_translatable fields to the API schema, a Translation
struct, and the translate_tweet client method with cookie-session auth.
Three related RSS improvements:
1. UserRssKind::Replies was grouped with Tweets, calling get_user_tweets
instead of get_user_tweets_and_replies. Add a dedicated
user_tweets_and_replies_v2_vars builder for the OAuth path.
2. Remove duplicate strip_html and get_link wrappers from rss.rs — import
strip_html from layout and tweet_link from renderutils directly.
3. Change render_thread_rss to accept &[&Tweet] instead of &Tweets to
avoid cloning conversation tweets into a new Vec. The thread_rss route
handler now builds Vec<&Tweet> references and inlines the Min-Id
response header since rss_response expects &[Tweet].
TweetResultByIdQuery returns 404 for many tweets. Switch get_tweet to
delegate to get_conversation (TweetDetail endpoint) and return the main
tweet. The /i/status/{id} handler now renders the full conversation page
directly instead of redirecting, matching X.com client-side behaviour.
Remove the now-dead TweetResultByIdQuery endpoint, tweet_vars builder,
TweetResultData schema struct, parse_tweet function, and cache_keys::tweet.
Retweets page (/{user}/status/{id}/retweets) lists users who retweeted
via the Retweeters GraphQL endpoint, with cursor-based infinite scroll.
Quotes page (/{user}/status/{id}/quotes) shows quoting tweets via search
for quoted_tweet_id:{id}. Both pages share a back-arrow + tab bar header.
The tweet stats row now links retweet count to the retweets page and shows
quote count (when >0) linking to the quotes page. infiniteScroll.js updated
to detect engagement pages and use the .timeline container.
Maps Twitter's TweetDetail `rankingMode` variable (Relevance/Recency/Likes)
to a `?sort=` query param. The dropdown sits inside the main tweet's stats
row via TweetRenderer.sort_toggle(), uses CSS :focus-within for no-JS
open/close. Non-default sorts bypass the conversation cache. Also filters
promoted items inside conversationthread entries that were slipping through.