First commit

This commit is contained in:
2026-03-25 20:50:17 -03:00
commit c3f3efbf33
6 changed files with 626 additions and 0 deletions

48
CLAUDE.md Normal file
View File

@@ -0,0 +1,48 @@
# 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, 11000, 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` (01000), `parentHeight` (01000)
- 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