diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 70 | ||||
| -rw-r--r-- | docs/MESHBAY_HTTP_API.md | 233 | ||||
| -rw-r--r-- | docs/QUICKSTART.md | 2 | ||||
| -rw-r--r-- | docs/USERGUIDE.md | 47 | ||||
| -rw-r--r-- | docs/generate_http_api.py | 164 |
5 files changed, 500 insertions, 16 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index e638f9b..2cfe7a4 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -17,7 +17,7 @@ > it. §13 is the register of those labels. > > Wire versions at the time of writing: **MNP 6.0** (oldest peer accepted 4.0), -> **MHP 0.1**, packages **0.17.0**. The normative source for the wire format is +> **MHP 0.1**, packages **0.18.0**. The normative source for the wire format is > `MESHBAY_NODE_PROTOCOL.md`; this document states the design the protocol > serves, not its byte layout. @@ -33,6 +33,7 @@ | `QUICKSTART.md` | one machine to a working group, for somebody who has installed nothing | | `USERGUIDE.md` | using a group and running a node, for the person who does either | | `MESHBAY_NODE_PROTOCOL.md` | the MNP wire format, message by message | +| `MESHBAY_HTTP_API.md` | every route of the hub and of the node's control API, generated from the code | | `transfers-v1.md` | the transfer system's failure-mode analysis, kept because a synthesis cannot carry "every way a slot can be lost" | | `playlists.md` | the playlist design and its interface in full, with what building it corrected (§9.10) | | `cast-smart-tv.md` | the DLNA/UPnP device backend — designed, not built (§11.4) | @@ -1888,7 +1889,8 @@ is not authentication: any local process can reach it, as can a page in the operator's browser via DNS rebinding — and this API re-initialises group keys, issues invitations and reads the audit log. There is no server-rendered dashboard; the desktop client's Node page and the CLI are the two consumers, and each -operation endpoint is one `_op(...)` line onto `ops` (§5.4). +operation endpoint is one `_op(...)` line onto `ops` (§5.4). Its routes are listed +in `MESHBAY_HTTP_API.md`. **The node's own controls are not on MNP.** Its status — which lists every group on the machine with each root's absolute path — settings, roster, denylist and @@ -1950,6 +1952,8 @@ an operator-signed op. ## 7. The hub +Its routes are listed in `MESHBAY_HTTP_API.md`. + ### 7.1 Role — chosen, not minimal Hub minimisation was considered and **deferred, and may be dropped** (decision D4). @@ -2880,6 +2884,16 @@ There is no folder-browsing protocol and this does not add one. application with no toolbar renders none — an empty band still holds a strip of the page open. +9. **Licence.** An application may be under any licence, provided it reaches the + interface only through the *application interface*: the props of §9.2, the + registry fields above, the exports of `i18n.js`, `icon.js`, `file-utils.js`, + `settings-ui.js` and `folder-tree.js`, `style.css`'s classes and the + catalogues' keys (`static/licenses/APPLICATION-EXCEPTION.txt`, an AGPL §7 + permission). Importing any other module of the interface makes the + application a work based on it, under the AGPL. Its registry line and its + catalogue entries are changes to the interface and stay AGPL. Starting from + `helloworld-app.js`, which is 0BSD, brings no AGPL code along. + No protocol change, no hub change, no daemon change. Steps 4 and 7 are the only node-side and test-side touches, and both are allow-lists. @@ -3422,6 +3436,58 @@ lands the target is shown, not the old stream's position. Pausing the receiver pauses the local element too, which otherwise goes on fetching for nobody. The copy-URL cast has no receiver to read and keeps the ordinary player. +**A television is chosen for the session, not for one film** +(`cast-session.js`). Any cast button — Videos', Photos' and Music's toolbars, a +photo album's bar, the lightbox, the video player, the music bar — sets it, and +it is held in memory only. A film opened +while one is set starts its cast as a restart does: pending until the first +segment, then the relay and `connect` (or `reload`, when the receiver is already +connected), with the player drawn as the remote from the start. *Stop casting* +clears it, from the player's remote or from any button. + +**A photo or a music track is served whole at `/file`, one at a time** +(`cast:file:begin`, `:write`, `:end`), its cover at `/cover`. It crosses in +pieces — binary frames of a megabyte on Android — because a lossless track is +too large for one bridge message, and on Android it is written to the relay's +spool directory rather than held. The relay starts for it if nothing else has, +serves it behind the stream's token with a versioned address (a receiver handed +the same URL twice shows what it already has), honours byte ranges (a receiver +seeks in a track that way), and accepts only pictures and the audio types the +music player plays. **The shell, not the page, says what the receiver loads:** +the relay's file is loaded as what the relay was told it is — a photo as a +picture (`streamType: NONE`), a track as music (`BUFFERED`, with title, artist, +album and the relay's cover URL), at `startAt` seconds for a track picked up +mid-song — any other relay URL as the stream, and on Android no URL but the +relay's own is accepted at all. A file does not start the foreground service; a +photo's load answers as soon as the receiver accepts it, since a photo never +reaches PLAYING. + +**A photo is scaled before it leaves.** The page fits it to 1920×1080, applies +its EXIF orientation and re-encodes it as JPEG: a camera original is too large +for a receiver to decode in good time, and some formats it does not decode at +all. Only the newest photo asked for is sent; paging quickly shows where the +reader stopped. + +**With a television chosen, the music bar is its remote.** Each track, once +decrypted, goes to the relay with its cover instead of to the `<audio>` +element, which keeps the source for its duration only. Play, pause, seek +(`cast:chromecast:seek`) and previous drive the receiver; the bar's clock is the +receiver's, polled once a second; and the receiver's IDLE/FINISHED moves the +queue on as `ended` does, once per track, because the receiver keeps reporting +it until it is given something else. The session records which view the +television is showing (`castSession.owner`): a film or a photo opened meanwhile +takes it, the bar pauses and stops reading the receiver, and play gives it the +track back from where it was. A television chosen mid-track picks the track up +where it was; *Stop casting* carries it on locally from where the television +was. + +**The interface itself is not cast.** The default media receiver plays what it +is given and renders nothing of its own, and a rendered interface sent to it as +live video does not work: it starts a progressive stream that arrives at real +time only when the stream carries audio and its first seconds arrive faster than +real time, and then holds that lead as latency — five seconds, measured. Showing +the application on a television needs a registered receiver of our own. + **Subtitles are rebased onto the relay's clock before they are sent.** The node extracts a track whole, so its cues carry the film's timeline, and the player can use them unchanged because its SourceBuffer is given `timestampOffset = diff --git a/docs/MESHBAY_HTTP_API.md b/docs/MESHBAY_HTTP_API.md new file mode 100644 index 0000000..74b9d77 --- /dev/null +++ b/docs/MESHBAY_HTTP_API.md @@ -0,0 +1,233 @@ +# MeshBay HTTP API + +> **Generated** by `docs/generate_http_api.py` from the routes themselves. Do not +> edit this file: change the route's docstring and run the script again. A test +> fails when the two disagree. + +MeshBay has two HTTP APIs: the **hub's**, for accounts, groups and signaling, and +the **node's control API**, which only its own machine reaches. Files, the index +and chat do not use either: they travel between a client and a node over MNP +(`MESHBAY_NODE_PROTOCOL.md`). Why each API is shaped as it is, who may call +what and what the hub must never see are in `MESHBAY_DESIGN.md`; this file lists +what exists. + +`examples/` has small Python programs that use both. + +## Hub + +Under the hub's address, `https://meshbay.org` on the reference deployment. The +hub also serves the web application at `/` and `/app/`, which are not listed. + +| Auth | What the request carries | +|---|---| +| none | No session. Any credential is in the request itself, as the route says | +| user | `Authorization: Bearer`, a person's session. A node's token is refused | +| user or node | A person's session, or a node daemon's token from `/v1/nodes/auth` | +| node | A node daemon's token only | +| moderator | A person's session, for an account with the moderator or admin role | +| admin | A person's session, for an account with the admin role | +| peer hub | Another hub's MHP token. Every route answers 503 unless federation is enabled | + +### Instance + +| Method | Path | Auth | What | +|---|---|---|---| +| GET | `/v1/hub/info` | none | Versions and the instance policy a client needs before signing in. | +| GET | `/v1/hub/pubkey` | none | Hub Ed25519 public key PEM — cached by nodes on first contact. | +| GET | `/v1/hub/version` | none | Version check endpoint for clients to detect updates. | + +### Accounts + +| Method | Path | Auth | What | +|---|---|---|---| +| POST | `/v1/users/register` | none | Create an account. | +| POST | `/v1/users/verify-email` | none | Verify a registration email with the code received by mail. | +| POST | `/v1/users/me/bundle-pepper` | user | The pepper, for a session that has just been given the passphrase again. | +| POST | `/v1/users/login` | none | Sign in with the auth key derived from the passphrase. | +| POST | `/v1/users/devices` | user | Register a device's hub authentication key. | +| GET | `/v1/users/devices` | user or node | The account's registered devices. | +| DELETE | `/v1/users/devices/{device_id}` | user | Retire a device's hub key. | +| POST | `/v1/users/auth` | none | Sign in with a registered device key. | +| POST | `/v1/users/token/refresh` | none | Exchange a refresh token for a new session. | +| GET | `/v1/users/me` | user or node | The signed-in account: id, name, e-mail, role, status. | +| PATCH | `/v1/users/me` | user | Update the signed-in account. | +| POST | `/v1/users/verify-email-change` | user | Confirm an email change with the code sent to the new address. | +| POST | `/v1/users/logout` | none | End this session on the hub, not only in the browser. | +| POST | `/v1/users/me/sessions/revoke` | user | Sign out everywhere: no refresh token of this account renews any more. | +| POST | `/v1/users/password` | user | Change the passphrase, proving the current one. | +| POST | `/v1/users/password/reset-request` | none | Send a reset code by e-mail, when the username and the address match. | +| POST | `/v1/users/password/reset` | none | Set a new passphrase with the code received by e-mail. | +| GET | `/v1/users/me/preferences` | user or node | The account's stored interface preferences. | +| PUT | `/v1/users/me/preferences/{key:path}` | user | Store one interface preference. | +| DELETE | `/v1/users/me/preferences/{key:path}` | user | Remove one interface preference. | +| PUT | `/v1/users/me/node_key` | user | Link a node daemon's Ed25519 public key to the operator's account. | +| DELETE | `/v1/users/me/node_key` | user or node | Remove the linked node key from the operator's account. | +| DELETE | `/v1/users/me` | user | Erase your own account. | +| GET | `/v1/users/{username}/pubkeys` | user or node | Resolve a username to its account id, and its node's linking key. | + +### Nodes + +| Method | Path | Auth | What | +|---|---|---|---| +| POST | `/v1/nodes/mnp-token` | user | Mint the short-lived token a member presents to a node in the MNP handshake. | +| POST | `/v1/nodes/auth` | none | Authenticate a node daemon via Ed25519 challenge-response. | +| POST | `/v1/nodes/announce` | user or node | Register a node record. | +| GET | `/v1/nodes/{node_id}` | user or node | A node's public record: owner, key, endpoint hint. | + +### Groups + +| Method | Path | Auth | What | +|---|---|---|---| +| GET | `/v1/groups/mine` | user or node | List groups the current user belongs to. | +| GET | `/v1/groups/invitations` | user or node | Groups somebody added this account to, waiting for it to say yes. | +| POST | `/v1/groups/{group_id}/invitation/accept` | user | Accept an invitation: the account becomes a member of the group. | +| POST | `/v1/groups/{group_id}/invitation/decline` | user | Decline an invitation to a group. | +| POST | `/v1/groups/{group_id}/activity` | user or node | Bump a group's last_activity_at. | +| GET | `/v1/groups/{group_id}/nodes` | user or node | Return online nodes that serve a group (for WebRTC connection). | +| GET | `/v1/groups` | none | List/search public groups — local and optionally federated. | +| GET | `/v1/groups/{group_id}/members` | user or node | A group's members, for its members only. | +| POST | `/v1/groups/{group_id}/join` | user | Join an open group. | +| POST | `/v1/groups` | user | Create a group, owned by the caller. | +| DELETE | `/v1/groups/{group_id}/members/{username}` | user | Remove someone from a group. | +| POST | `/v1/groups/{group_id}/leave` | user | Leave a group you are a member of. | +| PATCH | `/v1/groups/{group_id}` | user | Change the group's description. | +| POST | `/v1/groups/{group_id}/members/{username}` | user or node | Add an account to a group the caller owns. | +| POST | `/v1/groups/{group_id}/mute` | user | Turn this group's notifications on or off, for this account. | +| DELETE | `/v1/groups/{group_id}` | user | Delete a group. | +| POST | `/v1/groups/{group_id}/invite-notify` | user | Send an invitation email to a member who was just invited. | +| GET | `/v1/groups/{group_id}/hosts` | user | The nodes that host a group or asked to, for its owner. | +| POST | `/v1/groups/{group_id}/hosts/{node_id}` | user | Approve a node that asked to host this group. | +| DELETE | `/v1/groups/{group_id}/hosts/{node_id}` | user | Withdraw an approval, or turn a request down. | + +### Invitation links + +| Method | Path | Auth | What | +|---|---|---|---| +| POST | `/v1/groups/{group_id}/invite-links` | user or node | Mint the ticket for a link whose node half already exists. | +| GET | `/v1/groups/{group_id}/invite-links` | user or node | The owner's view: the links nobody has used yet, masked. | +| DELETE | `/v1/groups/{group_id}/invite-links/{link_id}` | user or node | Take the ticket back. | +| POST | `/v1/invite-links/preview` | user | What the confirmation screen shows before anyone joins anything. | +| POST | `/v1/invite-links/redeem` | user | Membership for the first account that asks, once — and the same answer again for that account, because a second tab or a reload is the same person. | + +### Revocation and the node socket + +| Method | Path | Auth | What | +|---|---|---|---| +| WS | `/v1/nodes/ws` | none | Persistent WebSocket connection for nodes, authenticated by the node's token in the first message. | +| POST | `/v1/nodes/{node_id}/incoming` | user or node | Signal a node that a client wants to connect (NAT punch coordination). | +| POST | `/v1/admin/revoke` | admin | Revoke a user or group. | + +### Moderation + +| Method | Path | Auth | What | +|---|---|---|---| +| POST | `/v1/reports` | user | Report a file of a public group, as a member of that group. | +| GET | `/v1/blocklist` | node | The content blocklist, a page at a time, for a node hosting a public group. | +| GET | `/v1/admin/blocklist` | admin | The content blocklist. | +| POST | `/v1/admin/blocklist` | admin | Add a content hash (BLAKE3) to the blocklist. | +| DELETE | `/v1/admin/blocklist/{content_hash}` | admin | Remove a content hash from the blocklist. | +| GET | `/v1/admin/reports` | moderator | Hashes waiting for a decision, oldest first, with what was said about them. | +| POST | `/v1/admin/reports/{content_hash}/block` | admin | Block reported content and close its reports. | +| POST | `/v1/admin/reports/{content_hash}/dismiss` | admin | Dismiss the reports on a piece of content. | + +### Federation (MHP) + +| Method | Path | Auth | What | +|---|---|---|---| +| GET | `/mhp/info` | peer hub | Return this hub's identity for peer registration. | +| GET | `/mhp/directory` | peer hub | This hub's public groups, for a peer hub presenting an MHP token. | +| POST | `/mhp/directory` | peer hub | A peer hub's public groups, pushed with a single-use MHP token. | +| POST | `/mhp/revoke` | peer hub | Act on a revocation from a peer hub. | +| POST | `/mhp/peers` | admin | Admin: register a trusted peer hub. | +| GET | `/mhp/peers` | admin | Admin: list registered peer hubs. | + +### Health + +| Method | Path | Auth | What | +|---|---|---|---| +| GET | `/v1/health` | none | Liveness: database reachable, version, connected nodes. | + +### Signaling + +| Method | Path | Auth | What | +|---|---|---|---| +| POST | `/v1/nodes/{node_id}/webrtc/offer` | user or node | Browser sends WebRTC SDP offer for a node. | + +### Administration + +| Method | Path | Auth | What | +|---|---|---|---| +| GET | `/v1/admin/settings` | moderator | Instance-wide policy an admin controls from the panel. | +| PATCH | `/v1/admin/settings` | admin | Change instance policy. | +| GET | `/v1/admin/mail` | moderator | Is the hub still sending, and how much of the hour is left. | +| GET | `/v1/admin/stats` | moderator | Account, group and node counts. | +| GET | `/v1/admin/users` | moderator | Search and list accounts. | +| GET | `/v1/admin/users/{user_id}` | moderator | One account, with its group count. | +| PATCH | `/v1/admin/users/{user_id}` | moderator | Change an account's status, or its role (admin only). | +| DELETE | `/v1/admin/users/{user_id}` | admin | Erase an account, and every group it owns. | +| GET | `/v1/admin/groups` | moderator | List groups with their member counts. | +| PATCH | `/v1/admin/groups/{group_id}` | moderator | Change a group's status. | +| GET | `/v1/admin/nodes` | moderator | Registered nodes, with the address the hub saw them announce from. | +| GET | `/v1/admin/logs` | moderator | The connection log, filtered by account and event. | + +### Notifications + +| Method | Path | Auth | What | +|---|---|---|---| +| GET | `/v1/notifications` | user or node | The account's notifications, newest first. | +| POST | `/v1/notifications/{notification_id}/read` | user or node | Dismiss one. | +| DELETE | `/v1/notifications/{notification_id}` | user or node | Dismiss one. | +| DELETE | `/v1/notifications` | user or node | Throw them all away. | +| POST | `/v1/notifications/read-all` | user or node | Dismiss every one — the same thing as `DELETE ""`, under the name an older client knows it by. | + +## Node control API + +`http://127.0.0.1:<ui_port>`, port 18000 unless `ui_port` in `node.toml` says +otherwise, and never on another address. Every request carries the token the +daemon writes to `<data_dir>/ui-token` (`~/.local/share/meshbay/ui-token` on +Linux), as the `X-MeshBay-Token` header or the `t` query parameter. The daemon +draws a new one at each start and deletes the file when it stops. + +| Method | Path | What | +|---|---|---| +| GET | `/api/status` | The daemon's state, and what it still needs: a linked key, a group, an operator, a group key. | +| DELETE | `/api/unlink` | Unlink the node's key from its hub account. | +| GET | `/api/groups` | The groups this node hosts, with live status, and whether an operator is paired. | +| POST | `/api/groups/attach` | Host a group that exists on the hub: add it to node.toml with its first folder, then reload. | +| POST | `/api/groups/detach` | Stop hosting a group: remove it from node.toml, then reload. | +| DELETE | `/api/groups/{group_id}/files/{file_id}` | Delete a file from the group's folder on disk. | +| GET | `/api/denylist` | What the node currently refuses. | +| POST | `/api/denylist/clear` | Drop denylist entries: all of them, or one identifier. | +| GET | `/api/index-cache` | Size of the index cache. | +| POST | `/api/index-cache/prune` | Drop index cache rows that no longer match a file on disk. | +| POST | `/api/groups/{group_id}/video/rematch` | Forget the automatic matches of the group's videos, so they are looked up again. | +| GET | `/api/groups/{group_id}/files` | The group's files, from its index. | +| GET | `/api/peers` | The connected peers. | +| GET | `/api/audit` | The audit log, filtered by time, account and event. | +| POST | `/api/operator/pair` | A one-time code that pairs an application as this node's operator. | +| GET | `/api/roster` | The pinned identities, for one group or all. | +| POST | `/api/groups/{group_id}/invites` | An invitation code for one account, for this group. | +| POST | `/api/groups/{group_id}/invite-links` | A whole invitation link: the node's code, then the hub's ticket. | +| DELETE | `/api/groups/{group_id}/invite-links/{invite_id}` | Take an invitation link back, on the node and on the hub. | +| GET | `/api/resolve` | Map a username to an account id, through the hub. | +| POST | `/api/members/{user_id}/revoke` | Stop serving the group key to a member. | +| POST | `/api/members/{user_id}/unpin` | Forget a pinned identity, so the person can pair again with a new key. | +| GET | `/api/groups/{group_id}/chat` | What the operator needs to decide anything about the group's chat. | +| POST | `/api/groups/{group_id}/chat/epoch` | Open a new chat epoch. | +| POST | `/api/groups/{group_id}/chat/encrypt-history` | Re-encrypt the messages written before the group's chat was encrypted. | +| POST | `/api/groups/{group_id}/chat/prune` | Delete chat messages older than a number of days. | +| POST | `/api/groups/{group_id}/gek` | Generate the group key, or rotate it with ?rotate=true. | +| POST | `/api/groups/{group_id}/roots` | Add a folder to a group. | +| PATCH | `/api/groups/{group_id}/roots/{root_name}` | Make a folder writable or removable, or not. | +| PUT | `/api/groups/{group_id}/roots/{root_name}/eject` | Eject a removable folder so its disk can be unplugged. | +| PUT | `/api/groups/{group_id}/roots/{root_name}/plug` | Bring an ejected folder back. | +| DELETE | `/api/groups/{group_id}/roots/{root_name}` | Remove a folder from a group. | +| GET | `/api/groups/{group_id}/index-status` | One group's indexing progress. | +| GET | `/api/index-status` | Every group's indexing progress. | +| PUT | `/api/groups/{group_id}/apps` | Which applications members see for the group. | +| POST | `/api/reload` | Reload node.toml. | +| POST | `/api/shutdown` | Stop the daemon. | +| GET | `/api/node-settings` | The node's effective settings. | +| PUT | `/api/node-settings` | Change node settings, written to roster.db and node.toml. | +| GET | `/api/transfers` | Live transfer leases and queue depth. | +| PUT | `/api/groups/{group_id}/transfer-limits` | How many transfers one member may run at once in this group. | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index ce1eec1..ea75cab 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -359,7 +359,7 @@ A healthy node reads roughly like this: ``` hub https://meshbay.org (user yourname) node key 7mK2p...= -daemon running — ok +daemon running node_id 82.65.x.x:0 groups 1 files 4213 peers 1 config /home/you/.config/meshbay/node.toml diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index 1790475..97ea679 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -352,7 +352,9 @@ An album browser and a player, over the folders the operator chose for it. ### Photos An album browser over the folders the operator chose for it, where **an album -is a folder**. Thumbnails come from the node, already rotated correctly. +is a folder**. Thumbnails come from the node, already rotated correctly. A +photo opens full size, with a slideshow button that moves on every five +seconds and stops at the album's last photo. **Location data is never shown.** Photos shows when a picture was taken and what took it, and no coordinates anywhere. Worth knowing, though: the @@ -392,15 +394,32 @@ resume. ### Casting to a TV -**The desktop and Android applications.** A film playing in the application can -be sent to a cast-capable TV or dongle on the same network: *Cast to device* in -the player, pick one from the list. On a phone, the film goes on playing silently -on the phone while the TV shows it, and the screen can be turned off. While a -TV plays the film, the player becomes its remote: the position shown is the -TV's, with play/pause, 30-second jumps and a slider to go anywhere in the film. -Devices appear as they answer, usually within a couple -of seconds; the search carries on for a few more, for a TV that is still -waking up. +**The desktop and Android applications.** Films, photos and music can be sent +to a cast-capable TV or dongle on the same network. The cast button is in the +Videos and Music toolbars, at the top of Photos, in an album's bar, in the photo +viewer and in the music bar — in a group and in Search alike — and *Cast to +device* is in the video player. Devices appear as +they answer, usually within a couple of seconds; the search carries on for a +few more, for a TV that is still waking up. + +**A TV, once chosen, stays chosen** until *Stop casting*, from any cast button +or from the player. Every film opened plays on it, and the player opens as its +remote: the position shown is the TV's, with play/pause, 30-second jumps and a +slider to go anywhere in the film. Every photo opened in the viewer is shown on +it, and a slideshow pages the TV along with the screen. Choosing a TV from an +album's bar opens its first photo. Music plays on it too, with the cover, title +and artist on the screen: the music bar becomes its remote — play, pause, the +slider, previous and next — and the queue moves on when the TV reaches the end +of a track. Choosing the TV in the middle of a song carries it on there; *Stop +casting* carries it on here. On a phone, a film goes on playing silently on the +phone while the TV shows it, and the screen can be turned off, as it can while +music plays on the TV. + +Photos are sent scaled to the TV's screen, upright, as an ordinary picture. +The menus, covers and summaries stay on your phone or computer: the TV shows +what is playing, not the application. It plays it in the TV's standard cast +player, which shows its own name, *Default Media Receiver*, on the screen; a +player of MeshBay's own is not built. The application decrypts the film and relays it to the TV itself, over your LAN. The TV is not a group member and holds no key — which is also why the relay's @@ -974,13 +993,15 @@ Better to know now than to go looking for it: - **The Android application is not released yet.** It works — groups, chat, films, downloads, casting — but is built and installed by hand and signed with a development key, so there is no store page and no update channel. The - back button and the lock screen do not drive it yet. A phone browser works - too. + back button and the lock screen do not drive it yet. Music keeps playing + with the screen off, from one track to the next, with a notification shown + while it plays. A phone browser works too. - **A node cannot be hosted on Android**, and is not planned to be. - **Hubs do not talk to each other yet.** Everyone in a group needs an account on the same hub. - **Casting reaches Chromecast devices** from the desktop and Android - applications. Support for other TV protocols is designed but not built. + applications, for films, photos and music. Support for other TV protocols is + designed but not built. - **Some subtitle tracks cannot be shown** — the ones stored as images rather than text, roughly one embedded track in five. Displaying them would need text recognition. diff --git a/docs/generate_http_api.py b/docs/generate_http_api.py new file mode 100644 index 0000000..d064519 --- /dev/null +++ b/docs/generate_http_api.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +Write docs/MESHBAY_HTTP_API.md from the routes of the hub and of the node's +control API. + + python docs/generate_http_api.py + +Generated rather than written: a list of a hundred routes kept by hand is wrong +by the next one added. `test_http_api_doc.py` fails when the file and the code +disagree, and when a route has no docstring to describe it. +""" + +import inspect +import re +import sys +from pathlib import Path + +from fastapi.routing import APIRoute, APIWebSocketRoute + +OUT = Path(__file__).resolve().parent / "MESHBAY_HTTP_API.md" + +# The hub's routers, by module, in the order the hub includes them. +SECTIONS = { + "hub": "Instance", + "users": "Accounts", + "nodes": "Nodes", + "groups": "Groups", + "invite_links": "Invitation links", + "revocation": "Revocation and the node socket", + "moderation": "Moderation", + "federation": "Federation (MHP)", + "health": "Health", + "signaling": "Signaling", + "admin": "Administration", + "notifications": "Notifications", +} + +# Strongest first: a route is labelled by the first dependency it carries. +AUTH = [ + ("require_admin", "admin"), + ("require_moderator", "moderator"), + ("require_node_scope", "node"), + ("require_user_scope", "user"), + ("get_current_user", "user or node"), + ("_decode_token", "token"), + ("_federation_open", "peer hub"), +] + +AUTH_LEGEND = """\ +| Auth | What the request carries | +|---|---| +| none | No session. Any credential is in the request itself, as the route says | +| user | `Authorization: Bearer`, a person's session. A node's token is refused | +| user or node | A person's session, or a node daemon's token from `/v1/nodes/auth` | +| node | A node daemon's token only | +| moderator | A person's session, for an account with the moderator or admin role | +| admin | A person's session, for an account with the admin role | +| peer hub | Another hub's MHP token. Every route answers 503 unless federation is enabled | +""" + +HEADER = """\ +# MeshBay HTTP API + +> **Generated** by `docs/generate_http_api.py` from the routes themselves. Do not +> edit this file: change the route's docstring and run the script again. A test +> fails when the two disagree. + +MeshBay has two HTTP APIs: the **hub's**, for accounts, groups and signaling, and +the **node's control API**, which only its own machine reaches. Files, the index +and chat do not use either: they travel between a client and a node over MNP +(`MESHBAY_NODE_PROTOCOL.md`). Why each API is shaped as it is, who may call +what and what the hub must never see are in `MESHBAY_DESIGN.md`; this file lists +what exists. + +`examples/` has small Python programs that use both. +""" + +HUB_INTRO = """\ + +## Hub + +Under the hub's address, `https://meshbay.org` on the reference deployment. The +hub also serves the web application at `/` and `/app/`, which are not listed. + +""" + +NODE_INTRO = """\ +## Node control API + +`http://127.0.0.1:<ui_port>`, port 18000 unless `ui_port` in `node.toml` says +otherwise, and never on another address. Every request carries the token the +daemon writes to `<data_dir>/ui-token` (`~/.local/share/meshbay/ui-token` on +Linux), as the `X-MeshBay-Token` header or the `t` query parameter. The daemon +draws a new one at each start and deletes the file when it stops. + +| Method | Path | What | +|---|---|---| +""" + + +def flatten(routes): + for r in routes: + if hasattr(r, "original_router"): + yield from flatten(r.original_router.routes) + elif isinstance(r, (APIRoute, APIWebSocketRoute)): + yield r + + +def summary(route) -> str: + """The docstring's first sentence.""" + doc = inspect.getdoc(route.endpoint) or "" + first = " ".join(doc.split("\n\n")[0].split()) + return re.split(r"(?<=[.!?])\s+(?=[A-Z`])", first)[0].replace("|", "\\|") + + +def method(route) -> str: + if isinstance(route, APIWebSocketRoute): + return "WS" + return ", ".join(sorted(route.methods - {"HEAD"})) + + +def auth(route) -> str: + names = set() + + def walk(dependant): + for d in dependant.dependencies: + if d.call is not None: + names.add(getattr(d.call, "__name__", "")) + walk(d) + + walk(route.dependant) + return next((label for name, label in AUTH if name in names), "none") + + +def hub_routes() -> list: + from meshbay_hub.app import create_app + return [r for r in flatten(create_app().routes) + if r.endpoint.__module__.rsplit(".", 1)[-1] != "webapp"] + + +def node_routes() -> list: + from meshbay_node.ui.app import create_ui_app + return list(flatten(create_ui_app({}).routes)) + + +def render() -> str: + out = [HEADER, HUB_INTRO, AUTH_LEGEND] + by_module: dict[str, list] = {} + for r in hub_routes(): + by_module.setdefault(r.endpoint.__module__.rsplit(".", 1)[-1], []).append(r) + for module, routes in by_module.items(): + out.append(f"\n### {SECTIONS.get(module, module.replace('_', ' ').capitalize())}\n\n") + out.append("| Method | Path | Auth | What |\n|---|---|---|---|\n") + for r in routes: + out.append(f"| {method(r)} | `{r.path}` | {auth(r)} | {summary(r)} |\n") + out.append("\n" + NODE_INTRO) + for r in node_routes(): + out.append(f"| {method(r)} | `{r.path}` | {summary(r)} |\n") + return "".join(out) + + +if __name__ == "__main__": + OUT.write_text(render(), encoding="utf-8") + print(f"wrote {OUT}", file=sys.stderr) |