ipx-directory: the directory of public podcasts ipx servers share (#204)
A new crate and service, crates/directory, live at ipx-dir.sdf1.net. ipx servers send the public feeds their people subscribe to (POST /v1/snapshot, replacing what that token sent before); the directory fetches each feed's title, artwork, category, description and latest twenty items every six hours with ipx-feed, and lists them A to Z and by subscribers summed over every server. It holds metadata only, never a file or a file's address. Tokens are in its config file as SHA-256 hashes, each ro, rw or rwa, and the file is read again within a minute of a change, so adding and revoking one is an edit and nothing else; a revoked token takes its server's counts. At first they were to come from environment variables, which would have meant recreating the container to add one. A file that does not load keeps the tokens as they were. Every address sent is checked with ipx_feed::url_looks_private, the check ipx makes before sending, so one server's miss does not publish a paid feed to every other; an rwa token can remove and block what both miss. Deployed: ipx-directory in the ipx Arcane project, database ipx_directory (its own login) on the databases project's Postgres, which db-backup already covers, and a tunnel route with no Access in front. The store's tests run on Postgres as well as SQLite when IPX_TEST_DATABASE_URL is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
99
docs/directory.md
Normal file
99
docs/directory.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# The directory
|
||||
|
||||
`ipx-directory` is a catalogue of public podcasts and blogs that ipx servers share (#204). An ipx
|
||||
server sends it the public feeds its people subscribe to, and how many of them do. The directory
|
||||
fetches each feed's title, artwork, category, description and latest twenty items every six hours,
|
||||
and any server holding a token can browse it. It never downloads a file, and it never stores
|
||||
one's address: every ipx server still fetches its own feeds.
|
||||
|
||||
This server's is at `https://ipx-dir.sdf1.net`, the `ipx-directory` service of the Arcane project
|
||||
`ipx`, with its database `ipx_directory` on the `databases` project's Postgres.
|
||||
|
||||
## Tokens
|
||||
|
||||
Everything but `/healthz` needs a token, sent as `Authorization: Bearer <token>`. Each one has a
|
||||
name and one of three levels of access:
|
||||
|
||||
| | browse | send snapshots | remove, block, refresh feeds; drop a server's snapshot |
|
||||
|---|---|---|---|
|
||||
| `ro` | yes | | |
|
||||
| `rw` | yes | yes | |
|
||||
| `rwa` | yes | yes | yes |
|
||||
|
||||
Tokens are in the config file and nowhere else, and no level of access can make or revoke one
|
||||
over the API. To make one:
|
||||
|
||||
```sh
|
||||
docker exec ipx-directory ipx-directory token new <name> --access ro|rw|rwa
|
||||
```
|
||||
|
||||
It prints the token, to give to whoever it is for, and the lines to add to the config file. The file
|
||||
holds the token's SHA-256, never the token. The directory reads the file again within a minute of
|
||||
a change, so adding a token and revoking one (deleting its entry) need no restart. A revoked token
|
||||
also takes the counts its server sent. A file that does not load (a misspelled `access`, two tokens
|
||||
with one name) leaves the tokens as they were, and the log says why.
|
||||
|
||||
## The config file
|
||||
|
||||
`/config/directory.toml` in the container (`DIRECTORY_CONFIG`, or `--config`), on Tower
|
||||
`/mnt/fast/appdata/ipx-directory/directory.toml`:
|
||||
|
||||
```toml
|
||||
bind = "0.0.0.0:8100"
|
||||
# The environment variable holding the database URL: postgres://... or sqlite://...
|
||||
database_url_env = "DIRECTORY_DATABASE_URL"
|
||||
|
||||
[[tokens]]
|
||||
name = "tower"
|
||||
access = "rwa"
|
||||
token_sha256 = "…"
|
||||
```
|
||||
|
||||
`DIRECTORY_LOG_FORMAT=json` logs one JSON object a line, for Loki, as ipx's `IPX_LOG_FORMAT` does.
|
||||
Each request is a line with its `method`, `path`, `status`, `duration_ms` and the `token`'s name.
|
||||
|
||||
## Running one
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ipx-directory:
|
||||
image: ipx-directory # docker build -f Dockerfile.directory -t ipx-directory .
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DIRECTORY_DATABASE_URL: "postgres://ipx_directory:password@db-host:5432/ipx_directory"
|
||||
ports:
|
||||
- "8100:8100"
|
||||
volumes:
|
||||
- ./directory:/config:ro # directory.toml
|
||||
healthcheck:
|
||||
test: ["CMD", "ipx-directory", "health"]
|
||||
```
|
||||
|
||||
## What it will not hold
|
||||
|
||||
A private feed is never meant to reach it. ipx leaves out a feed marked private, one with a
|
||||
username or password, and one whose address `ipx_feed::url_looks_private` catches: a paid-feed
|
||||
host, or a key in the query. The directory checks every address it is sent with the same function
|
||||
and refuses what it catches, so one server's miss does not publish someone's paid feed to every
|
||||
other. What both miss, an `rwa` token can remove: `DELETE /v1/feeds/{id}` removes the feed and
|
||||
blocks it, so no snapshot brings it back.
|
||||
|
||||
## API
|
||||
|
||||
| Route | Access | |
|
||||
|---|---|---|
|
||||
| `GET /healthz` | none | `ok` when it can read its database |
|
||||
| `GET /v1/me` | ro | the token's `name` and `access` |
|
||||
| `GET /v1/feeds` | ro | every feed A to Z: `id`, `url`, `title`, `image`, `category`, `subcategory`, `podcast`, `subscribers` |
|
||||
| `GET /v1/popular?limit=10` | ro | the most subscribed, at most 100 |
|
||||
| `GET /v1/feeds/{id}` | ro | one feed, with `description`, `failing` (why its last check failed, in words) and its latest twenty `items` (`guid`, `title`, `link`, `published`, `duration`, `description`), never their files |
|
||||
| `POST /v1/snapshot` | rw | `[{"url", "subscribers"}]`, at most 20,000: replaces everything this token sent before; answers what it `accepted` and how many it refused as private, blocked or invalid |
|
||||
| `DELETE /v1/feeds/{id}` | rwa | remove and block |
|
||||
| `GET /v1/blocked`, `DELETE /v1/blocked/{id}` | rwa | what is blocked; unblock |
|
||||
| `DELETE /v1/instances/{name}` | rwa | drop one token's snapshot, its counts with it |
|
||||
| `POST /v1/feeds/{id}/refresh` | rwa | fetch the feed now, answering with its page |
|
||||
|
||||
A feed's `id` is the first 64 bits of the SHA-256 of its address, normalised (scheme and host in
|
||||
lower case, no default port or fragment), so every server arrives at the same id. `subscribers`
|
||||
is the sum over every token's latest snapshot. A feed no snapshot has any more goes. Descriptions
|
||||
are the feed's own HTML, unsanitized: whoever shows them sanitizes them, as ipx does.
|
||||
Reference in New Issue
Block a user