Default signing keys made media proxy URLs forgeable, and unauthenticated
media fetches let arbitrary upstream hosts consume server-side network
access. This commit makes those boundaries explicit, limits buffered upstream
bodies, and keeps externally sourced text escaped at raw rendering sinks.
Users can set their Kagi session token in preferences to use Kagi
Translate instead of Twitter's Strato API. The /translate endpoint
reads the kagiToken cookie and routes accordingly, falling back to
Strato when no token is set.
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.
The .alt-text paragraph had no styling, causing it to flow inline and
overlap adjacent images. Position it as an absolute overlay at the
bottom-left of each image attachment.
Match FxTwitter style: abbreviated stats (38.8K), reordered to
replies/retweets/likes/views, blockquote formatting for quote tweets,
and article:published_time for footer timestamp.
format_with_commas produced garbled output for negative numbers because
it reversed the string including the minus sign. Rewrite to handle the
sign separately and iterate forward, also pre-allocating the result.
Change replace_youtube_urls and replace_reddit_urls to return Cow<str>
so the common case (no replacement configured) avoids allocation. Update
replace_urls_abs to call .into_owned() on the final Cow.
Five header .parse().unwrap() calls on user-controlled session data could
panic on malformed values. Replace with .map_err(?) to surface a proper
Error::Internal instead. Also make the search cursor end-detection safe
by using .get(..64) instead of indexing, which would panic on short
cursors.
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].
timeline.rs and rss.rs each had a private get_link fn that just called
renderutils::tweet_link. Remove the wrappers and import tweet_link
directly. (rss.rs changes follow in a later commit.)
short_url compared truncated.len() (byte length) against stripped.len()
(byte length), but truncated was built with .chars().take(max_len) which
counts code points. For multi-byte characters the byte lengths could
mismatch and produce wrong truncation. Compare char counts instead.
list_by_slug and list_by_id had identical blocks filtering retweets from
timeline groups. Extracted into a shared maybe_filter_retweets function.
Also replaces !is_some_and with is_none_or per clippy preference.
Return 404 instead of session pool internals when enable_debug is false.
Removes the typed return values (Json<HealthResponse>) in favor of
Response to support the early 404 return.
The condition always omitted the port when https was true, even for
non-standard ports like 8443. Now checks against the default port for
the active scheme (443 for HTTPS, 80 for HTTP) so non-default ports
are correctly included in the URL prefix.
Sessions marked as globally rate-limited were never unmarked, causing
them to be skipped forever. Add a GLOBAL_LIMIT_DURATION_SECS (15 min)
timeout to SessionLimits::is_limited so expired limits are ignored, and
clear the limited flag in update_session_limit when a session responds
successfully after its limit window has elapsed.
Fetches the conversation via TweetDetail and collects before + main +
self-thread tweets into a flat RSS feed. Adds a cache key, RSS icon link
on status pages, and render_thread_rss() view helper.
Adds system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica, and Arial before the sans-serif fallback. Applies to both
the base stylesheet and the Twemoji override.
When a session's token is rejected (InvalidToken/BadToken), mark it as
limited and retry the request once. The session pool then picks the next
available session, providing automatic failover without caller changes.
Preferences checkboxes, text inputs, selects, and search form controls
now have proper id/for associations for screen readers. Adds .sr-only
class for visually-hidden labels on the search input and date fields.
Enables per-user filtering and styling via uBlock/adblock rules. For
example, `.author-elonmusk { display: none }` hides all tweets from
that user, including when they appear as retweets in your timeline.
Restricts script/style/media sources to self-origin, blocks eval() and
inline scripts, and prevents framing. Uses if_not_present so individual
routes can override if needed.
404 means the resource doesn't exist; 204 (No Content) correctly signals
that pagination reached the end. The JS fetch handler now checks for any
non-2xx status instead of only 404, so it stops scrolling on 204 too.
The Twitter API returns the last page verbatim when no more results exist.
Compare the first 64 chars of the input cursor with the returned bottom
cursor to detect this and clear the timeline, stopping infinite scroll
from looping. Mirrors upstream nitter fixes 2898efab and 4bf3df94.
Without parens, multi-user OR groups leak into adjacent filter expressions,
producing incorrect Twitter search queries. Mirrors upstream nitter fix
b0773dd9. Also wraps the joined filter list in parens and short-circuits
to the raw param when no filters are present.
The url_prefix included the listen port even behind a reverse proxy, producing unreachable og:image URLs like https://host:7963/pic/... that broke Discord/Slack embeds. When https = true the public port is always 443, so the port is now unconditionally omitted.