KydoimosandClaude Opus 5 583c489074
CI / test (pull_request) Successful in 40s
Use cURL for Bluesky API calls instead of file_get_contents
FreshRSS 1.30.0 added unregister_unsafe_protocols() to lib/lib_rss.php,
which unregisters every PHP stream wrapper except file:// and php:// at
boot as an SSRF mitigation. That removed the https:// wrapper, so the
stream-context-based fetch in apiGet() stopped working entirely — every
API call failed with "Unable to find the wrapper https".

Because the call site used @file_get_contents(), the failure was silent:
apiGet() returned null, loadThread() fell through to its null fallback,
and entries were saved with their unenriched RSS content. The visible
symptom was Bluesky posts rendering with no thread and no embeds, with
nothing in the logs to explain it.

Switch apiGet() to cURL, which does not go through stream wrappers (and
is how FreshRSS fetches feeds itself, which is why feed retrieval kept
working throughout). Connect timeout added and redirect-following
explicitly disabled — a public XRPC endpoint has no reason to redirect,
and following redirects would reopen an SSRF vector.

Also stop swallowing failures: transport errors, non-2xx responses and
malformed JSON are now reported via Minz_Log::warning() so this class of
breakage is visible next time.

Tests: add testApiGetWorksWithoutHttpsStreamWrapper, which unregisters
the https wrapper before calling the API and so fails against the old
implementation, plus testApiGetLogsWarningOnFailure. Add a Minz_Log stub
and curl to the CI extension list.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 10:32:55 -03:00
2026-03-25 20:50:17 -03:00
2026-03-25 20:50:17 -03:00
2026-03-25 20:50:17 -03:00

Bluesky Threads — FreshRSS Extension

A FreshRSS extension that enriches Bluesky posts in your RSS feeds by fetching the full reply thread and embedded content via the Bluesky public API, collapsing everything into a single article.

Features

  • Fetches full reply threads when a post is first saved, so API sync clients (Fever, GReader, etc.) receive enriched content immediately
  • Automatically refreshes thread content as it ages, with progressively relaxed refresh intervals
  • Renders rich text with working links, mentions, and hashtags
  • Renders all embed types: images, external link cards, quoted posts, and videos
  • No Bluesky account or API credentials required

Installation

  1. Download or clone this repository into FreshRSS's extensions/ directory. The folder must be named xExtension-BlueskyThreads.
  2. In FreshRSS, go to Extensions and enable Bluesky Threads.

Configuration

In the extension settings, you can set Thread depth (default: 10, max: 1000) — how many levels of replies to fetch per post.

How it works

Hooks

Two hooks work in tandem so both the web UI and API sync clients receive enriched content:

  • EntryBeforeInsert — fires once when a new entry is first saved. Fetches the thread immediately and stores the rendered HTML.
  • EntryBeforeDisplay — fires on every web render. Checks if the cached thread is stale; if so, re-fetches and writes the updated HTML back to the database.

Cache & staleness

Thread HTML is cached as JSON files in DATA_PATH/BlueskyThreads/{md5(url)}.json. Staleness is determined by the post's age:

Post age Cache refresh interval
< 1 hour Every 10 minutes
< 24 hours Every 1 hour
< 7 days Every 12 hours
7+ days Never (frozen)

On API failure, the stale cache is served as a fallback.

API endpoints used

All requests go to public.api.bsky.app — no authentication required.

  • Handle resolution: com.atproto.identity.resolveHandle
  • Thread fetch: app.bsky.feed.getPostThread
S
Description
A FreshRSS extension that enriches Bluesky posts in your RSS feeds by fetching the full reply thread and emb edded content via the Bluesky public API, collapsing everything into a single article.
Readme
135 KiB
Languages
PHP 98.5%
HTML 1.5%