Files
meta-feeder-usenet/README.md
T

182 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# meta-feeder-usenet
Usenet feeder sidecar for the MetaMesh gateway tier. Bridges a Usenet header
scan **we run ourselves** — the **NNTmux** app ([nntmux](https://github.com/NNTmux/newznab-tmux),
worph/AppStore) — into the MetaMesh network, over nntmux's Newznab API.
## Why this exists
`meta-feeder-indexer` proxies an **external** Newznab indexer and mints an
`nzb-release` (`0x1005`) cid that embeds *that indexer's host*. Only a peer
holding that indexer's credential can ever redeem it — so the metadata half of
Usenet is neither self-owned nor portable.
Scanning Usenet ourselves gives us the article Message-IDs for free at collation
time (no grab, no scrape). That lets this feeder mint a **portable**
`nzb-posting` (`0x1003`) cid — a digest over the Message-ID set, embedding no
host — which **any peer with a plain NNTP provider can redeem, with no indexer
credential anywhere in the path**.
This feeder **adds** a Usenet metadata source alongside `meta-feeder-indexer`.
It replaces nothing; a gateway query fans out to both.
It also **redeems external releases.** The torznab feeder mints `nzb-release`
(`0x1005`) locators from Prowlarr/Newznab search hits; when a viewer plays one,
the gateway asks this feeder (`POST /compute` with the locator cid) to grab its
`.nzb` with the key configured here for that indexer host. The Newznab keys live
on this feeder — meta-share keeps only the NNTP provider. See
[Redeeming nzb-release locators](#redeeming-nzb-release-locators).
Design record:
`meta-gateway/docs/others/self-hosted-usenet-indexer-study.md`.
## Shape
```
[NNTmux app: PHP + MariaDB + Redis] [usenet-feeder: Rust, FeederPlugin]
header-scan groups (few conns) ◄── t=search&q=… (Newznab API, `metamesh` key)
collate → releases ◄── /getnzb?id=<guid> once per release → cache
writes <guid>.nzb.gz mints 0x1003 from the Message-ID set
(CBP purged; the .nzb is the only emits the record + a relative
durable home of the msg-ids) `manifest_url` onto its own /blob route
│
▼
gateway core: seeds the .nzb as an ORDINARY sha2-256 IPFS cid, publishes it
as a fileType=nzb meta-core record, rewrites the field to that cid
(the same path posters take — `remote_feeder.rs` SEEDABLE_FIELDS)
│
▼
meta-share: resolve `manifest` cid (blockstore / bitswap / ANY holder) →
NzbManifest::from_nzb_xml → BODY-fetch each Message-ID from ANY plain NNTP
provider → materialise → play
```
### nntmux over its API, not its database (1.1)
Until 1.1 the feeder read nntmux's MariaDB with SQL and gunzipped
`<guid>.nzb.gz` off nntmux's storage volume. That made the two a single unit:
the feeder held nntmux's DB password, depended on its table layout and its NZB
shard depth, and had to mount its storage. nntmux is now its own app, and the
feeder uses the same contract any Newznab client uses (the torznab feeder does
the same with Prowlarr):
| Need | Call | nntmux counter |
|---|---|---|
| search | `GET {nntmux_url}/api/v1/api?t=search&q=…&extended=1&apikey=…` | `apirequests` |
| a release's `.nzb` (Message-IDs) | `GET {nntmux_url}/getnzb?id=<guid>.nzb&r=<key>` | `downloadrequests` |
| compute (by guid) | `t=details&id=<guid>` | `apirequests` |
| reachability (`/health`) | `t=caps` (no key) | — |
- ⚠ The API is under **`/api/v1/api`**; `/api` is a 404. `t=get` redirects to
`/getnzb` on the *public* `APP_URL` host, so the feeder calls `/getnzb` on the
internal host directly.
- Every search hit must carry its `0x1003` cid (else meta-watch shows
"Unavailable"), so a hit costs one `/getnzb` **the first time it is seen**.
Each `.nzb` is cached under `<state>/gateway/usenet/nntmux-nzb/<guid>.nzb`
(a collated release never changes) and reused by repeat searches, `/compute`
and `/blob`.
- nntmux caps every role per 24 h (Admin 1000/1000, User 10/10) and throttles
`/api` to 60/min. Use the NNTmux app's dedicated **`metamesh`** user (its role
has raised caps); the feeder paces itself to ≤ 50 requests/min, and stops a
query's downloads once nntmux reports a cap (`500`/`501`).
Like every feeder this one is **meta-core-free and blockstore-free**: it finds
records and serves bytes; the core owns hashing-into-the-blockstore, the
meta-core store-back, and the libp2p wire (gateway invariant 10).
## Configuration
Operator config lives on the **feeder**, not the gateway (invariant 12). Set it
on the plugin's config page in the gateway dashboard; the env vars below are a
**first-boot seed only** — the persisted `config.json` wins on the next restart,
and there is no hot reload.
| Field | Env seed | What |
|---|---|---|
| `nntmux_url` | `NNTMUX_URL` | Base URL of the NNTmux app as this container reaches it (default `http://nntmux`); the API path is appended. |
| `nntmux_api_key` | `NNTMUX_API_KEY` | API key of the nntmux user the feeder searches as (the `metamesh` user). Blank → self-scan search is off. |
| `indexers` | — (config page only) | `[{host, api_key}]` Newznab keys for redeeming `nzb-release` locators. `host` is the bare authority (`api.nzbgeek.info` — no scheme, no `/api`). |
Both halves are optional; with neither configured the plugin reports Degraded,
as it does when nntmux is unreachable or refuses the key. A grab-only deployment
(keys, no nntmux) is valid. A pre-1.1 `config.json` (`db_url`/`nzb_root`) is
ignored with a warning.
Infra env: `META_FEEDER_HTTP_LISTEN` (default `0.0.0.0:8080`),
`META_FEEDER_STATE_DIR` (default `/data/meta-feeder`), `RUST_LOG`.
Scan policy — which groups, backfill depth, scan interval, connection budget —
is the **gateway maintainer's** call and is configured in the NNTmux app.
Keep the connection budget conservative: the scanner and playback draw on the
same provider slots, so a greedy value does not merely over-scan, it stalls
playback mid-stream.
## Redeeming nzb-release locators
`POST /compute {upstream_id: "usenet", record_id: <0x1005 cid>}`:
1. Decode `{api_base, id}` from the cid; find the key for its host. No key →
`404` (not ours — the gateway tries another claimer, nothing is spent).
2. `GET https://{api_base}/api?t=get&id=…&apikey=…` with a named User-Agent
(nzbgeek answers `109 Invalid User Agent` without one). **This spends the
indexer's daily download quota** — the gateway only calls it on a real play,
and never again for a release whose `manifest` it already stored.
3. Strip the `.nzb` `<head>` except `<meta type="password">` (the file is seeded
to the whole mesh; the password is needed to unpack a protected RAR set).
4. Mint the portable `nzb-posting` (`0x1003`) cid from its Message-IDs.
5. Answer ONE `sha2_256` outcome: the `.nzb` bytes (`file_extension: nzb`) and a
record carrying `cids/<nzb-posting>` + `segmentCount`. The gateway stores the
file under `/files/plugin/meta-feeder-usenet/`, writes its cid onto the release
record as `manifest`, and merges the posting cid — after which any peer with a
plain NNTP provider can play the release.
Errors: an indexer credential/account refusal (`100`–`102`, `910`) or an HTML
page → `422`; request/download limit (`429`, `500`, `501`, or HTTP 429) → `429`;
no such item (`300`) → `404`.
`GET /redeems` and `/manifest` advertise `{codec: nzb-release, field: manifest,
hosts: [...]}` for the hosts that have a key; nothing without keys.
## Build
```bash
cargo build --release --bin usenet-feeder
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
docker build -f feeder-plugin/usenet-feeder/Dockerfile -t ghcr.io/worph/meta-feeder-usenet:dev .
```
`meta-feeder-sdk` is a git dependency pinned by tag (`v1.2.2`; this repo used to
vendor a copy). An SDK change is invisible here until it is tagged and pushed,
and cargo cannot resolve an unpushed tag — not even under a `[patch]` override. To verify against a local SDK checkout, build a
scratch copy with the dependency swapped for a path (never commit that form —
this repo is the Docker build context):
```bash
SCRATCH=/d/workspace/tmp-claude/usenet-feeder; SDK=/d/workspace/MetaMesh/meta-root-v2/packages/plugins/meta-feeder-sdk
rm -rf $SCRATCH && mkdir -p $SCRATCH && tar cf - --exclude=./target --exclude=./.git . | (cd $SCRATCH && tar xf -)
sed -i "s#meta-feeder-sdk = { git = .*#meta-feeder-sdk = { path = \"$SDK\" }#" $SCRATCH/feeder-plugin/*/Cargo.toml
docker run --rm -e RUSTUP_TOOLCHAIN=1.89.0 -e CARGO_TARGET_DIR=/target -v /d/workspace:/d/workspace \
-v feeder-cargo-registry:/usr/local/cargo/registry -v usenet-feeder-target:/target -w $SCRATCH \
rust:1.89 sh -c 'apt-get install -y -qq pkg-config libssl-dev >/dev/null 2>&1; cargo test'
```
After the tag is pushed, run `cargo update -p meta-feeder-sdk` here so
`Cargo.lock` stops pointing at the removed vendored path.
`compute_nzb_posting_cid`'s normalisation rule is the dedup contract and is
mirrored on the meta-share side — change them together.
## Gotchas
- **An empty catalog is the visible symptom of both nntmux silent-zero traps**:
an empty `PATH_TO_NZBS` (set-but-empty beats the default, so `createNZBs()`
aborts and you get 0 releases with no error), and seeded groups left
`active=0`. The plugin logs a loud warning when `releases` is empty at boot —
believe it.
- **The Message-IDs are not in the database.** nntmux purges
collections/binaries/parts right after writing each NZB, so the on-disk
`.nzb.gz` is the only durable home. A wrong `nzb_root` therefore fails at
`compute_outcomes`, not at query time.