Files
gerbeur/API.md
khannurien 51439f475d
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 51s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 26s
v4: the GeoCities marquee scrolls the community feed, what the site's members are doing — it crawled three canned lines, a welcome, "best viewed in Netscape Navigator" and "under construction", which said nothing of the site. The feed is a list any style may show (CommunityFeed.tsx), which GeoCities lays out along its marquee: who is online, the week's dumps, upvotes, comments and chat messages, linking to /community, its most upvoted dump, then the 12 latest things done in public, newest first, each ending on how long ago it was: a public dump posted, a public playlist created, a comment or an upvote on a public dump, someone joining and who invited them. Every name and title links to its page. A dump has one comment line and one upvote line at most, its latest, so that a thread or a popular dump doesn't fill the feed. GET /api/community/feed serves it, signed in only (community-service.ts): the events are read from the tables on each request, the week's figures are those /community already keeps, as of its last refresh. A signed-out visitor has no marquee. CommunityFeedProvider holds the feed around every page, so that it outlives the header each page renders anew, fetches it only while a component shows it, and fetches it again two seconds after the socket last told of a dump, a comment, a vote, a playlist, a rename, a deletion or a reconnection: the socket's messages don't carry a feed's lines, and the server's query stays the one place that decides what the feed holds. Someone joining comes with no message, and shows at the next of those. The crawl goes at 50 pixels a second however much the feed holds, Marquee.tsx measuring one copy for the stylesheet to work the loop's length out from, where a fixed 36 seconds would have sped up with every line. A loop shows what the feed held when it began: text that changed mid-crawl would jump, so what came meanwhile waits for the next loop, which also brings the times up to date. The next page's marquee carries on from where the last one's had got to, rather than from the start. The marquee was hidden from assistive technology as decoration; its first copy is now a labelled list of links, the second, which the seamless loop needs, hidden and out of the tab order. A link that takes the focus stops the crawl and the line scrolls to it, the pointer pauses it as before, and with reduced motion the line shows once, still, follows the feed as it changes and scrolls by hand. The sentences a notification also says, "posted", "upvoted", "commented on", share its translations. The tests' DOM gets an AnimationEvent before React loads (src/test/before-react.ts): finding none, React listened for the animation events under their WebKit names, and a test's "animationiteration" reached no handler.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:38:44 +00:00

303 lines
20 KiB
Markdown

# gerbeur's URLs
Every page and every API endpoint follows one of two rules. This file summarizes
them. Each router's module doc in `api/routes/` lists its own endpoints in full,
and a route change updates both. `shared/paths.ts` builds and parses the page
paths.
## The two rules
**Pages.** The path is a hierarchy. `@name` is a user, and `@name@host` a user
whose account lives on another instance. Any other word under the root is a
category or an app page. Under a user, each segment is a fixed tab name, and
under `dumps` or `playlists`, a slug. A view of a page is one more segment. A
default view is the bare path, with no redirect. A category is a segment where
the page is the category's own, a dump category's feed at the root (`/music`)
and a playlist category's under `/playlists` (`/playlists/mixes`); where it
narrows a list on another page, a profile's or a search's, it is `?category=`,
of the kind that list holds. The order of a list of playlists is `?sort=`, the
default order being the bare path, and the playlists page's list of the viewer's
own is `?mine=1`. Switching tabs, a tab's views or a list's order takes the
place of the current history entry; picking a category, or opening another page
or an editor, adds one.
**API.** A resource is `/api/<plural>/<key>[/<noun>]`. Its key is its id, except
a user's, which may also be `@name`. The `@` keeps the two apart, so no regex
sniffing is needed. A read by page path mirrors the page:
`/api/users/@bob/dumps/<slug>`. The caller's own things live under `/api/me`.
Media is a sub-resource. A lookup by another key is a query filter. Each concept
has one word, the same as in pages.
## Pages
| Path | Views | Access | Page |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------- |
| `/` | `/hot`, `/new`, `/journal`, `/follow`, `/follow/playlists`; the bare `/` is the viewer's default tab, the bare `/follow` the users followed | anyone, `/follow` signed in | `Index` |
| `/<category>` | `/<category>/hot`, `/new`, `/journal`, `/follow`, `/follow/playlists` | anyone, `/follow` signed in | `Index` |
| `/@<name>` | the overview: dumps beside upvoted | anyone | `UserPublicProfile` |
| `/@<name>/dumps`, `/upvoted`, `/playlists`, `/invitees` | `?category=` narrows the dumps, the upvoted or the playlists; `?sort=` orders the playlists | anyone | `UserPublicProfile` |
| `/@<name>/follow` | `/@<name>/follow/playlists`; the bare tab lists users | anyone | `UserPublicProfile` |
| `/@<name>/manage` | | `user:manage`, on others' profiles | `UserPublicProfile` |
| `/@<name>/dumps/<slug>` | | anyone; a private dump, its poster | `Dump` |
| `/@<name>/dumps/<slug>/edit` | | its poster, `dump:moderate` | `DumpEdit` |
| `/@<name>/playlists/<slug>` | | anyone; a private playlist, its owner | `PlaylistDetail` |
| `/@<name>/playlists/<slug>/edit` | | its owner, `playlist:moderate` | `PlaylistDetail` |
| `/playlists` | `/playlists/<category>`, a playlist category's; `?sort=new`, `?sort=dumps` order either; `?mine=1` lists the viewer's own, signed in only | anyone | `Playlists` |
| `/search?q=` | `/search/users?q=`, `/search/playlists?q=`; `?category=` narrows the dumps or the playlists | anyone | `Search` |
| `/notifications` | | signed in | `Notifications` |
| `/community` | `/community/monthly`; the bare path is the week | signed in | `Community` |
| `/settings` | | signed in | `Settings` |
| `/manage` | | `category:manage`, `federation:manage` | `Manage` |
| `/login`, `/register?token=` | `/login?next=<path>` comes back to a page | signed out | `UserLogin`, `UserRegister` |
| `/authorize?client_id=…` | | signed in, else via `/login?next=` | `Authorize` |
| `/federation/callback?code=&state=` | | anyone | `FederationCallback` |
| `/reset-password` | `/reset-password?token=`, from the emailed link, sets the new password | anyone | `ResetPassword` |
| `/unsubscribe?token=` | | anyone | `Unsubscribe` |
`/?share_url=` opens the create form on a link shared from Android, `?chat=open`
opens the chat over any page, `?chat=<message id>` on that message, and
`#comment-<id>` scrolls a dump's page to a comment. A signed-in page opened
signed out replaces itself with `/login?next=<the page>`.
## Names
- **Slugs** of categories, dumps and playlists are lowercase letters and digits
in words joined by single hyphens (`shared/slug.ts`). A category's is at most
50 characters, a dump's or a playlist's at most 60.
- **A dump's or a playlist's slug** is made from its title when it is created,
unique among its owner's items of that kind: a clash gets `-2`, `-3`. It
changes only when someone edits it: the edit forms propose `slugify(newTitle)`
when the title changes. Its owner may change it 3 times a day; moderators
(`dump:moderate`, `playlist:moderate`) may edit any slug, at any time.
- **A username** is 1 to 32 letters, digits and underscores, unique in any case.
A page writes it in its stored case, and another case still resolves. Users
can't rename themselves: those with `user:manage` rename anyone, from
`/@<name>/manage`.
- **A user from another instance** is `name@host` here: their name at home and
their home's host, with its port if it has one (`alice@music.example`). Their
name is their home's to change, which leaves the old handle an alias here too.
Mentions write the whole handle. A handle with this instance's own host, as
another instance writes a user from here (`bob@gerbeur.example`), names the
local `bob`, in a path, an API key or a mention.
- **Aliases.** A slug or a name left behind stays an alias for good, which
always names the item or user itself, so redirects never chain. Nobody else
can take one. An item can take back one of its own. Deleting an item frees its
aliases.
- **Redirects.** A page reached by an alias, or by a name in another case, gets
the item from the API under its current path and replaces its URL. The server
answers the same path with a 301 to crawlers and full page loads.
- **Reserved words.** A category can't take a word the root already uses:
`hot new journal follow search login register notifications reset-password
unsubscribe settings manage community playlists authorize federation api ws
assets emojibase`
(`api/lib/reserved-slugs.ts`). Dump and playlist slugs reserve nothing, and no
slug has an `@`.
## API
Every JSON endpoint answers the envelope of `shared/api.ts`:
`{ "success": true, "data": … }`, or
`{ "success": false, "error": { "code",
"message" } }` with the matching HTTP
status. A list answers `{ "items", "hasMore" }` and takes `?page=` (from 1) and
`?limit=` (20 by default, 100 at most). "Signed in" means a session token as
`Authorization: Bearer …`. Media can't send a header, so a private file,
thumbnail, waveform, cover or attachment takes the owner's media token as
`?token=`.
An image the API serves, an avatar, a thumbnail, a cover, an attachment or a
remote image, takes `?w=` and answers a WebP copy scaled down to that width,
snapped up to one of `shared/image.ts`'s (128, 320, 640, 960), and never wider
than the image; without it, the image as it was uploaded, a dump's thumbnail
alone being scaled to the widest by default. `?f=compat` asks for the copy as a
JPEG or PNG, for a mail client that reads no WebP. An image no copy can be made
of goes out as it is. JSON answers of a kilobyte or more are compressed, in
brotli or gzip, for a client that accepts it.
### me
- `GET /api/me` — signed in. The caller's own user.
- `PATCH /api/me` — signed in. Changes the description and email.
- `GET /api/me/media-token` — signed in. A media token.
- `GET`, `PATCH /api/me/settings` — signed in. The caller's settings.
- `PUT /api/me/password` — signed in, with the current one. A new password, and
fresh tokens.
- `PUT /api/me/avatar` — signed in. A new avatar (multipart).
- `GET /api/me/playlists?contains=:dumpId` — signed in. The caller's playlists,
and whether each holds the dump.
- `GET /api/me/follow` — signed in. The ids of the users and playlists followed.
- `PUT`, `DELETE /api/me/follow/users/:id`, `/api/me/follow/playlists/:id` —
signed in. Follows or stops following.
- `GET /api/me/notifications` — signed in. The caller's notifications.
- `POST /api/me/notifications/read` — signed in. Marks them all read.
- `GET /api/me/grants` — signed in. The other instances the caller's account
signs in to.
- `DELETE /api/me/grants/:host` — signed in. Withdraws one, ending the caller's
sessions there.
An account from another instance changes its password, email, avatar and name
there: those endpoints answer it 400.
### users
- `POST /api/users` — with an invite. Signs up.
- `GET /api/users?q=` — anyone. Up to 8 users whose name starts with `q`.
- `GET /api/users/:key` — anyone. A profile.
- `GET /api/users/:key/avatar` — anyone. The avatar.
- `GET /api/users/:key/dumps`, `/upvoted`, `/playlists` — anyone; private items
to their owner only. Their lists; `?category=` narrows one to a category of
its kind, and `?sort=` orders the playlists as `GET /api/playlists` does.
- `GET /api/users/:key/dumps/:slug`, `/playlists/:slug` — anyone; private items
to their owner only. An item by its page path, by an alias too.
- `GET /api/users/:key/follow/users`, `/follow/playlists` — anyone. What they
follow.
- `GET /api/users/:key/invitees` — anyone. The tree of users they invited.
- `GET /api/users/:key/home?path=` — anyone. Redirects to a page of a remote
user's home instance, their profile there by default.
- `PATCH /api/users/:key/role`, `/username` — `user:manage`. Another user's
role, or anyone's name, theirs included.
### dumps
- `POST /api/dumps` — signed in. Posts a link (JSON) or a file (multipart).
- `GET /api/dumps` — anyone. The feed; `?category=<slug>` narrows it, and
`?url=` answers the dumps of the same link instead.
- `GET /api/dumps?follow=users|playlists` — signed in. The feed's `follow` tab:
the dumps of the users, or of the playlists, the caller follows; `?category=`
narrows it too.
- `GET /api/dumps/:id` — anyone who may see it. A dump.
- `GET /api/dumps/:id/voters`, `/related`, `/playlists` — the same. Who upvoted
it, the dumps linked to or from it, the playlists holding it.
- `PUT /api/dumps/:id` — its poster, `dump:moderate`. Title, slug, note,
visibility, categories, link.
- `POST /api/dumps/:id/metadata` — the same. Fetches a link's preview again.
- `DELETE /api/dumps/:id` — the same. Deletes it.
- `GET`, `PUT /api/dumps/:id/file` — the file; a new one, by its poster or
`dump:moderate`.
- `GET`, `PUT`, `DELETE /api/dumps/:id/thumbnail` — the thumbnail; a custom one,
set or removed by its poster or `dump:moderate`.
- `GET /api/dumps/:id/waveform` — anyone who may see it. An audio file's
waveform, `{ "peaks" }`, for the player to seek on.
- `GET`, `POST /api/dumps/:id/comments` — anyone who may see the dump; posting
signed in. Its comments.
### playlists
- `POST /api/playlists` — signed in. Creates a playlist, filed under the
playlist categories it names.
- `GET /api/playlists` — anyone. Every public playlist; `?category=<slug>`
narrows it to a playlist category, and `?sort=updated|new|dumps` orders it:
the last to take a dump first (the default), the newest, or the fullest. The
caller's own private dumps count, and date their playlists, for them alone.
- `GET /api/playlists/:id` — anyone who may see it. A playlist and its dumps.
- `PUT`, `DELETE /api/playlists/:id` — its owner, `playlist:moderate`. Title,
slug, description, visibility, categories; deletion.
- `POST`, `DELETE /api/playlists/:id/dumps/:dumpId` — the same. Adds or removes
a dump.
- `PUT /api/playlists/:id/order` — the same. Reorders its dumps.
- `GET`, `PUT /api/playlists/:id/cover` — the cover; a new one, by its owner or
`playlist:moderate`.
### comments
- `PATCH`, `DELETE /api/comments/:id` — its author, `comment:moderate`. Edits or
deletes a comment.
- `GET /api/comments/:id/likers` — anyone. Who likes it.
### categories
- `GET /api/categories` — anyone. Every category, dump and playlist ones apart:
each has a `kind`, and its slug is unique within its kind.
- `POST /api/categories`, `PUT`, `DELETE /api/categories/:id` —
`category:manage`. Changes them.
### community
- `GET /api/community?period=weekly|monthly` — signed in. The site's top dumps,
new members, new public playlists and totals over the last week (by default)
or month, as of the server's last refresh.
- `GET /api/community/feed` — signed in. The community feed: the 12 latest
things done in public, newest first (a public dump posted, a public playlist
created, a comment or an upvote on a public dump, someone joining), and the
last week's totals and most upvoted dump, as of the server's last refresh. A
dump has one comment event and one upvote event at most, its latest. Every
reader gets the same feed.
### chat
- `GET /api/chat?before=&after=` — signed in. A page of messages. The chat
itself goes over `/ws`.
- A message from a moderator or an admin (`chat:everyone`) that says `@everyone`
notifies every user who doesn't have the chat open, and comes flagged
`mentionsEveryone`. No account may be named `everyone`.
### invites
- `POST /api/invites` — signed in. A new invite.
- `GET /api/invites/:token` — anyone. Whether it still works.
### sessions, password-resets, unsubscribe
- `POST /api/sessions` — anyone. Signs in.
- `POST /api/password-resets` — anyone. Emails a reset link.
- `PUT /api/password-resets/:token` — with the link's token. A new password.
- `POST /api/unsubscribe?token=` — with a digest's token. Turns it off. Its v3
path, `POST /api/users/unsubscribe`, stays for good, since sent emails'
`List-Unsubscribe` headers name it.
### federation, oauth, .well-known
Federated sign-in (`shared/federation.ts`): a user signs in here with their
account on another gerbeur instance, their home, by OpenID Connect's
authorization code flow with PKCE, with tokens signed by each instance's Ed25519
key. With `GERBEUR_FEDERATION_INCOMING=0`, `POST /api/federation/logins` and
`/sessions` answer 403; with `GERBEUR_FEDERATION_OUTGOING=0`,
`GET /api/oauth/clients` and `POST /api/oauth/codes` do. An instance is named by
its issuer, its API's origin, whose host is the one in its users' handles. Where
`instance` is asked for, the origin of its pages works too: their
`/.well-known/openid-configuration` names the issuer, whose own metadata must
name those pages back as where its `/authorize` is.
- `GET /.well-known/openid-configuration`, `/.well-known/jwks.json` — anyone.
The instance's metadata and public keys, bare JSON: the one exception to the
`/api/` rule, at the paths the standard fixes.
- `POST /api/federation/logins` — anyone. Starts a sign-in with an account on
another instance; one neither trusted nor already home to someone here needs
an invite.
- `POST /api/federation/sessions` — anyone. Finishes it, as a sign-in.
- `POST /api/federation/events` — another instance. Its signed news of one of
its users here: a new name, email or avatar, or the end of their sessions. A
4xx tells it to give up on the event, a 429 or a 5xx to try again.
- `GET /api/federation/peers` — anyone the trusted instances,
`federation:manage` every listed one.
- `PUT /api/federation/peers`, `DELETE /api/federation/peers/:host` —
`federation:manage`. Trusts or blocks an instance, or takes it off the lists.
- `GET /api/oauth/clients?client_id=` — signed in. The instance asking to sign
the caller in.
- `POST /api/oauth/codes` — signed in. The caller's consent, and a code.
- `POST /api/oauth/tokens` — another instance, form-encoded. Trades a code for
an identity token, bare JSON.
### search
- `GET /api/search?q=` — anyone. A page of dumps, and up to 20 users and
playlists. It finds both kinds of item at once, so `?category=` narrows the
dumps and `?playlistCategory=` the playlists.
### attachments
- `POST /api/attachments` — signed in. Stores an image.
- `GET /api/attachments/:id` — anyone who may see the text holding it.
### remote
- `GET /api/remote/preview?url=` — signed in. A link's preview.
- `GET /api/remote/image?url=` — anyone. A remote image through this site.
- `GET /api/remote/bandcamp?url=` — anyone. A Bandcamp page's tracks.
- `GET /api/remote/youtube?list=` — anyone. A YouTube playlist's tracklist.
### /ws
- `GET /ws?token=` — anyone; a session token signs it in. The WebSocket of live
updates and the chat (`shared/ws.ts`).