155 lines
8.1 KiB
Markdown
155 lines
8.1 KiB
Markdown
# meta-feeder-usenet
|
||
|
||
Usenet feeder sidecar for the MetaMesh gateway tier. Bridges a Usenet header
|
||
scan **we run ourselves** (an [nntmux](https://github.com/NNTmux/newznab-tmux)
|
||
sidecar) into the MetaMesh network.
|
||
|
||
## 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 sidecar: PHP + MariaDB + Redis] [usenet-feeder: Rust, FeederPlugin]
|
||
header-scan groups (few conns) ──► reads `releases` (searchname/size/guid)
|
||
collate → releases ──► gunzips <nzb_root>/<c>/<guid>.nzb.gz
|
||
writes <guid>.nzb.gz ──► mints 0x1003 from the Message-ID set
|
||
(CBP purged; the gz 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
|
||
```
|
||
|
||
The sidecar is **persistent cache, cheap to lose** — everything in it is
|
||
re-derivable by re-scanning. Same pattern as the Tribler sidecar: stateful
|
||
upstream, but the gateway still sees a stateless HTTP feeder.
|
||
|
||
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 |
|
||
|---|---|---|
|
||
| `db_url` | `NNTMUX_DB_URL` | MariaDB DSN of nntmux's catalog, e.g. `mysql://nntmux:secret@nntmux-db:3306/nntmux`. Blank → self-scan search is off. |
|
||
| `nzb_root` | `NNTMUX_NZB_PATH` | nntmux's NZB directory **as mounted into this container**. Must be the same volume nntmux's `PATH_TO_NZBS` points at, or every release resolves to `NotFound`. |
|
||
| `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.
|
||
A grab-only deployment (keys, no nntmux) is valid.
|
||
|
||
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 on the nntmux sidecar.
|
||
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.
|