# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project A FreshRSS extension that enriches Bluesky posts in RSS feeds by fetching the full reply thread and embedded post content via the Bluesky public API, collapsing everything into a single article. ## Extension structure FreshRSS expects the extension directory name to start with `x`. Required files: | File | Purpose | |---|---| | `metadata.json` | Extension metadata (`name`, `entrypoint` are required) | | `extension.php` | Main class — must be named `{entrypoint}Extension extends Minz_Extension` | | `configure.phtml` | Optional settings form; submitted values handled by `handleConfigureAction()` | The extension is installed by dropping this directory into FreshRSS's `extensions/` folder. ## How it works Two hooks work in tandem so both the web UI and API sync clients (Fever, GReader, etc.) receive enriched content: 1. **`EntryBeforeInsert`** (`fetchThread`) — fires once when a new entry is first saved to the DB. Fetches the thread immediately and stores it, so API clients get enriched content from the very first sync. 2. **`EntryBeforeDisplay`** (`refreshThread`) — fires on every web render. Checks the file cache for staleness; if stale, re-fetches and calls `FreshRSS_Factory::createEntryDao()->updateEntry($entry)` to write the refreshed HTML back to the DB, keeping API clients up to date. 3. **Staleness rules** (`needsRefetch`): posts ≥ 7 days old are frozen. Fresher posts use progressively tighter refresh windows — 10 min (< 1 h old), 1 h (< 24 h old), 12 h (< 7 d old). 4. **Cache:** JSON files in `DATA_PATH/BlueskyThreads/{md5(url)}.json`, each containing `{html, fetched_at}`. On API failure, the stale cache is served as a fallback. 5. **Handle → DID:** `GET https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle={handle}` (skipped if the handle is already a `did:` URI). 6. **Thread fetch:** `GET https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://{did}/app.bsky.feed.post/{rkey}&depth={depth}&parentHeight=0` 7. **Rendering:** `renderThread()` walks the recursive `threadViewPost` structure. Root post uses a bordered card style; replies are indented under a left border. Each post renders its richtext facets (links, mentions, hashtags) and embeds (images, external links, quoted posts, video). ## Key implementation details - **Facets** — Bluesky richtext uses UTF-8 *byte* offsets (`byteStart`/`byteEnd`). `applyFacets()` walks the raw PHP byte string directly rather than converting to characters first. - **Embed type normalisation** — the API returns `$type` values like `app.bsky.embed.images#view`; the `#view` suffix is stripped before the switch statement. - **User config** — `depth` (int, 1–1000, default 10) is stored via `setUserConfigurationValue`/`getUserConfigurationValue` and read in `handleConfigureAction()` on POST. - **No auth required** — all requests go to `public.api.bsky.app` and need no credentials. ## Bluesky API reference - Thread endpoint: `app.bsky.feed.getPostThread` — params: `uri` (AT-URI), `depth` (0–1000), `parentHeight` (0–1000) - Handle resolution: `com.atproto.identity.resolveHandle` — param: `handle` - Docs: https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread ## FreshRSS extension docs https://freshrss.github.io/FreshRSS/en/developers/03_Backend/05_Extensions.html