gotextlog

gotextlog
git clone https://git.ryansepassi.com/git/gotextlog.git
Log | Files | Refs | README

commit 05c83b36bfe40eba783b690f9353c89cd69cc305
parent 24a699fd7e3c497b882b21fb0091238e10c802b1
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 26 Aug 2026 04:27:22 -0700

Document the TUI API contract

Diffstat:
MREADME.md | 5+++--
Adoc/api.md | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 262 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md @@ -68,8 +68,9 @@ Environment overrides are `TEXTLOG_URL`, `TEXTLOG_TOKEN`, `TEXTLOG_THEME`, and `NO_COLOR`. Session and UI settings are stored in the platform user configuration directory. -See [doc/usage.md](doc/usage.md) for the complete controls, configuration, and -API coverage. +See [doc/usage.md](doc/usage.md) for the complete controls and configuration. +See [doc/api.md](doc/api.md) for the HTTP API contract used by the terminal +client. ## Development diff --git a/doc/api.md b/doc/api.md @@ -0,0 +1,259 @@ +# Textlog API used by the terminal client + +This document describes the HTTP API contract used by this version of +`gotextlog`. It is an integration guide for compatible Textlog instances, +not a description of the complete server API. The upstream instance publishes +its full, machine-readable contract at +[`/api/openapi.json`](https://textlog.cc/api/openapi.json). + +## Transport and instance selection + +The client treats the configured URL as an instance origin and sends API +requests below that origin: + +```text +<instance origin>/api/v1/<endpoint> +``` + +For example, `textlog --url https://notes.example/path` uses +`https://notes.example/api/v1`; any path, query, or fragment in the configured +URL is discarded. The configuration validator accepts HTTP and HTTPS URLs; +use HTTPS for instances reached over a network. + +Ordinary requests use: + +```http +Accept: application/json +Authorization: Bearer <token> # when a token is configured +Content-Type: application/json # when the request has a JSON body +``` + +The client sends its bearer token on public as well as authenticated endpoints. +The live endpoint instead requests `text/event-stream`. Request cancellation is +propagated through the Go context. + +## Authentication + +Textlog sign-in is an email-code exchange. Account creation is not available +through the API, so a user must already have an account on the selected +instance. + +1. `POST /auth/request` asks the instance to email a code. +2. `POST /auth/verify` exchanges the email and six-digit code for a session. +3. The client saves `data.token` and sends it as a bearer token. +4. At startup, `GET /me` restores the signed-in account. +5. `DELETE /auth/session` revokes the current session during sign-out. + +Changing the configured instance also changes which server receives the saved +bearer token. Credentials and accounts are local to an instance; this client +does not implement federation or cross-instance identity. + +In the endpoint tables, **optional** means the endpoint does not require a +session but receives the bearer token when one is available. **Required** means +the TUI invokes it for a signed-in operation. **None** identifies sign-in +endpoints normally called before a token exists. + +## Endpoint inventory + +All paths in this section are relative to `/api/v1`. + +### Feeds, search, and live data + +| Method and path | Auth | Request | TUI use and response | +| --- | --- | --- | --- | +| `GET /feeds/latest` | Optional | `limit`, optional `cursor` | Latest feed. Reads a paginated post collection. | +| `GET /feeds/hot` | Optional | `limit`, optional `cursor` | Hot feed. Reads a paginated post collection. | +| `GET /activities/for-you` | Required | `limit`, optional `cursor` | Personalized activity feed. Reads an activity collection. | +| `GET /activities/to-me` | Required | `limit`, optional `cursor` | Replies, mentions, and follows directed to the user. Reads an activity collection. | +| `GET /search` | Optional | Required `q`; `limit`; optional `cursor` | Searches public posts and reads a paginated post collection. | +| `GET /firehose` | Optional | No parameters | Live feed over server-sent events (SSE). | + +Feed and search screens request 20 records at a time. Their Next and Previous +commands retain the opaque cursors returned by the server. The client decodes +`has_unread` from activity responses but does not currently display it or mark +feed/activity records as read. + +The firehose returns server-sent events in this form: + +```text +event: post +data: {"id":123,"body":"hello",...} + +``` + +The `data` value can be either a post object or `{"data": <post>}`. The parser +supports multiline SSE data, comments, CRLF, and a final event without a blank +line. It ignores event types other than `post` and skips malformed post events. +A post event must include `id`, `body`, `created_at`, and +`author.handle`. When an active stream ends or fails, the TUI reconnects after +a short delay; pausing or leaving the live screen cancels it. The protocol does +not replay posts missed between connections. + +### Posts and threads + +| Method and path | Auth | Request | TUI use and response | +| --- | --- | --- | --- | +| `GET /posts/{id}` | Optional | Positive integer `id` | Opens a post or obtains thread ancestry. Reads a post envelope. | +| `GET /posts/{id}/replies` | Optional | `depth`, `limit`, optional `cursor` | Builds the recursive reply tree. Reads a paginated reply collection. | +| `POST /posts` | Required | `{"body": string}` or `{"body": string, "parent_id": integer}` | Creates a top-level note or reply. Reads the created post envelope. | +| `PATCH /posts/{id}` | Required | `{"body": string}` | Edits a post owned by the signed-in user. Reads the updated post envelope. | +| `DELETE /posts/{id}` | Required | No body | Deletes a post owned by the signed-in user. The client accepts `data.deleted`. | +| `POST /posts/{id}/report` | Required | `{"reason":"harassment"|"spam"|"impersonation"|"other"}` | Reports another user's post. The client accepts `data.reported`. | + +To render a thread, the TUI first gets the selected post. If it is a reply, it +uses `top_id` when available, otherwise follows `parent_id`, fetching +ancestors until it reaches the root. It then requests every reply page for the +root with `depth=5` and `limit=100`. Repeated pagination cursors are treated +as an error. Replies deeper than five levels are not fetched by this client. + +The compose editor currently limits submitted bodies to 280 runes and ten +lines, even though the Textlog service permits notes up to 500 characters. +The server remains responsible for authoritative validation and authorization. + +### Profiles and relationships + +| Method and path | Auth | Request | TUI use and response | +| --- | --- | --- | --- | +| `GET /users/{handle}` | Optional | URL-escaped `handle` | Loads public profile details in a data envelope. | +| `GET /users/{handle}/notes` | Optional | `limit`, optional `cursor` | Loads the profile's top-level notes. | +| `GET /users/{handle}/replies` | Optional | `limit`, optional `cursor` | Loads the profile's replies. | +| `GET /users/{handle}/following/users` | Optional | `limit`, optional `cursor` | Lists accounts followed by the profile. | +| `GET /users/{handle}/following/tags` | Optional | `limit`, optional `cursor` | Lists hashtags followed by the profile. | +| `GET /users/{handle}/followers` | Optional | `limit`, optional `cursor` | Lists accounts following the profile. | +| `GET /users/{handle}/blocks` | Required | `limit`, optional `cursor` | Lists blocks when viewing the signed-in user's profile. | +| `POST /users/{handle}/follow` | Required | No body | Follows an account; accepts `data.following`. | +| `DELETE /users/{handle}/follow` | Required | No body | Unfollows an account; accepts `data.following`. | +| `POST /users/{handle}/block` | Required | No body | Blocks an account; accepts `data.blocked`. | +| `DELETE /users/{handle}/block` | Required | No body | Unblocks an account; accepts `data.blocked`. | + +Opening a profile is a composite operation: the TUI requests the profile, +notes, replies, followed users, followed tags, and followers sequentially. It +also requests blocks when the profile belongs to the signed-in user. Each list +is requested once with `limit=100`; profile tabs do not currently paginate +past that first page. A failure in a required profile request fails the whole +profile load. Failure to load the optional blocks tab is ignored. + +The API client still contains support for the deprecated +`GET /users/{handle}/posts` alias, but the TUI uses `/notes` and never calls +the alias. + +### Hashtags + +| Method and path | Auth | Request | TUI use and response | +| --- | --- | --- | --- | +| `GET /tags/{tag}` | Optional | URL-escaped `tag`, without `#` | Loads hashtag details in a data envelope. | +| `GET /tags/{tag}/posts` | Optional | `limit`, optional `cursor` | Loads posts carrying the hashtag. | +| `GET /tags/{tag}/followers` | Optional | `limit`, optional `cursor` | Lists accounts following the hashtag. | + +Opening a hashtag requests all three endpoints sequentially with `limit=100` +for the two collections. The TUI displays the first page and does not currently +provide hashtag follow/block actions. + +### Session and account + +| Method and path | Auth | Request | TUI use and response | +| --- | --- | --- | --- | +| `POST /auth/request` | None | `{"email": string}` | Emails a sign-in code. The client accepts `data.sent`. | +| `POST /auth/verify` | None | `{"email": string, "code": string}` | Returns a session envelope containing a token, expiry, and current user. | +| `DELETE /auth/session` | Required | No body | Revokes the active token; the client accepts `data.revoked`. | +| `GET /me` | Required | No parameters | Loads the current account in a data envelope. | +| `PATCH /me` | Required | `{"bio": string}` | Updates the current account bio and returns the account envelope. | + +The TUI validates that sign-in codes contain six digits before sending them. +It stores the verified session token in its platform configuration directory +and clears it after successful revocation. The bio editor limits input to 1,000 +runes; the instance performs final validation. + +## Response shapes consumed by the client + +Most single-resource and mutation responses use an envelope: + +```json +{"data": {}} +``` + +Collections use cursor pagination: + +```json +{ + "data": [], + "pagination": {"next_cursor": null} +} +``` + +`next_cursor` is either an opaque string or `null`. The client sends a +nonempty value back unchanged as the next request's `cursor`. + +### Post and reply + +The client consumes these post fields: + +| Field | Type | Use | +| --- | --- | --- | +| `id` | integer | Post identity, navigation, and mutation target. | +| `top_id` | integer or null | Root lookup for a reply thread. | +| `parent_id` | integer or null | Reply relationship and parent navigation. | +| `body` | string | Rendered note text. | +| `created_at` | RFC 3339 timestamp | Ordering/display and live-event validation. | +| `reply_count` | integer | Reply count metadata. | +| `tags` | array of strings | Hashtag rendering and navigation. | +| `mentions` | array of strings | Mention rendering and navigation. | +| `url` | URL string | Open-in-browser action. | +| `api_url` | URL string | Decoded for API compatibility; not used for routing. | +| `author` | user reference | Author rendering, profile navigation, follow/block actions. | +| `parent` | post or null | Quoted parent context in feeds. | + +A reply contains the post fields plus integer `depth`, used to indent and fold +the thread tree. + +### User, profile, and hashtag + +A user reference contains `handle`, `url`, and `api_url`. The full profile +adds `bio`, `created_at`, `post_count`, `replies_count`, +`follower_count`, `following_user_count`, `following_tag_count`, +`following_count`, and optional `blocked_user_count` and +`blocked_tag_count`. + +A hashtag reference contains `tag`, `post_count`, `follower_count`, `url`, +and `api_url`. + +The current-user response contains `handle`, `email`, `bio`, +`email_verified`, and `can_post`. A verified session adds `token` and +`expires_at` around that user object. + +### Activity + +An activity collection adds `has_unread` and returns objects with `id`, +`type`, `created_at`, `unread`, and a type-dependent `payload`. The TUI +understands these activity types: + +- `post`, `reply`, and `mention`: payload decodes as a post. +- `user_follow`, `tag_follow`, and `signup`: payload supplies an `actor` + user reference and, when applicable, a `target.handle` or `target.tag`. + +Unknown or malformed activity payloads are skipped. + +## Errors and compatibility requirements + +Textlog errors are expected to have this form: + +```json +{"error":{"code":"rate_limited","message":"Slow down"}} +``` + +For a non-2xx response, the client records the HTTP status plus `error.code` +and `error.message`. If the body is absent or malformed, it uses +`http_error` and `HTTP <status>` (`stream_error` for the firehose). A +numeric `Retry-After` header is displayed to the user in seconds. Transport +failures use `network_error`; cancellation remains a context cancellation. + +A compatible instance must implement the routes and JSON fields used by the +screens it wants to support. JSON may include additional fields: Go's decoder +ignores them. Successful JSON responses must have valid field types and RFC +3339 timestamps where timestamps are consumed. + +The TUI does not currently call several upstream API families, including +drafts, polls and voting, tag follow/block mutations, activity read markers, +latest-feed read markers, explore, post unpublish, feed keys, archives, embeds, +or RSS/Atom feeds. Consult the selected instance's OpenAPI document for those +server capabilities.