244 lines
4.6 KiB
Markdown
244 lines
4.6 KiB
Markdown
# Client Integration Guide
|
|
|
|
This document describes the current server contract you can rely on when implementing the client.
|
|
|
|
## Base Auth Model
|
|
|
|
- REST endpoints under `/api/*` require `Authorization: Bearer <jwt>`.
|
|
- WebSocket gateway at `/gateway` also requires JWT.
|
|
- Gateway token sources:
|
|
1. `Authorization: Bearer <jwt>` (preferred when possible)
|
|
2. query fallback: `?token=<jwt>` or `?access_token=<jwt>` (useful for browser WebSocket clients)
|
|
|
|
## Gateway (`/gateway`)
|
|
|
|
### Connect
|
|
|
|
- URL: `ws://<host>:<port>/gateway` (or `wss://` behind TLS)
|
|
- On successful auth, server sends:
|
|
|
|
```json
|
|
{
|
|
"op": "ready",
|
|
"connectionId": "gw_1",
|
|
"userId": "alice@example.com",
|
|
"username": "alice"
|
|
}
|
|
```
|
|
|
|
If auth fails, handshake is rejected with HTTP error (`401`/`403`) and no WebSocket session is established.
|
|
|
|
### Client -> Server Messages
|
|
|
|
All frames are JSON text.
|
|
|
|
#### Subscribe
|
|
|
|
```json
|
|
{"op":"subscribe","channelId":12}
|
|
```
|
|
|
|
or
|
|
|
|
```json
|
|
{"op":"subscribe","channelIds":[12,13]}
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"op": "subscribed",
|
|
"channelIds": [12, 13],
|
|
"subscriptions": [12, 13]
|
|
}
|
|
```
|
|
|
|
If one or more channels do not exist:
|
|
|
|
```json
|
|
{
|
|
"op": "subscribed",
|
|
"channelIds": [12],
|
|
"subscriptions": [12],
|
|
"unknownChannelIds": [999]
|
|
}
|
|
```
|
|
|
|
#### Unsubscribe
|
|
|
|
```json
|
|
{"op":"unsubscribe","channelId":12}
|
|
```
|
|
|
|
or
|
|
|
|
```json
|
|
{"op":"unsubscribe","channelIds":[12,13]}
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"op": "unsubscribed",
|
|
"channelIds": [12],
|
|
"subscriptions": [13]
|
|
}
|
|
```
|
|
|
|
#### Heartbeat
|
|
|
|
Client may send:
|
|
|
|
```json
|
|
{"op":"ping"}
|
|
```
|
|
|
|
or
|
|
|
|
```json
|
|
{"op":"heartbeat"}
|
|
```
|
|
|
|
Server replies:
|
|
|
|
```json
|
|
{
|
|
"op": "pong",
|
|
"ts": "2026-02-20T12:34:56.000Z"
|
|
}
|
|
```
|
|
|
|
### Server -> Client Event Messages
|
|
|
|
After subscription, channel message mutations are pushed as:
|
|
|
|
#### Message Created
|
|
|
|
```json
|
|
{
|
|
"op": "event",
|
|
"event": "message.created",
|
|
"channelId": 12,
|
|
"message": {
|
|
"id": 101,
|
|
"content": "hello",
|
|
"authorId": "alice@example.com",
|
|
"authorUsername": "alice",
|
|
"authorAvatarUrl": null,
|
|
"authorHomeServer": "https://home.example.com",
|
|
"channelId": 12,
|
|
"createdAt": "2026-02-20T12:34:56.000",
|
|
"editedAt": null
|
|
},
|
|
"emittedAt": "2026-02-20T12:34:56.000Z"
|
|
}
|
|
```
|
|
|
|
#### Message Updated
|
|
|
|
```json
|
|
{
|
|
"op": "event",
|
|
"event": "message.updated",
|
|
"channelId": 12,
|
|
"message": { "...same message shape..." },
|
|
"emittedAt": "2026-02-20T12:35:56.000Z"
|
|
}
|
|
```
|
|
|
|
#### Message Deleted
|
|
|
|
```json
|
|
{
|
|
"op": "event",
|
|
"event": "message.deleted",
|
|
"channelId": 12,
|
|
"messageId": 101,
|
|
"emittedAt": "2026-02-20T12:36:56.000Z"
|
|
}
|
|
```
|
|
|
|
### Gateway Error Payload
|
|
|
|
Malformed input or unsupported ops return:
|
|
|
|
```json
|
|
{"op":"error","message":"..."}
|
|
```
|
|
|
|
Common causes:
|
|
- invalid JSON payload
|
|
- missing/invalid `op`
|
|
- unsupported op
|
|
- missing `channelId`/`channelIds` for subscribe/unsubscribe
|
|
|
|
## Message History Pagination (`GET /api/messages`)
|
|
|
|
### Query Params
|
|
|
|
- `channelId` (required, int)
|
|
- `limit` (optional, int, default `50`, max `100`)
|
|
- `before` (optional, message ID cursor)
|
|
- `after` (optional, message ID cursor)
|
|
|
|
Rules:
|
|
- Do not send `before` and `after` together.
|
|
- `before`/`after` must be positive integers.
|
|
|
|
### Response Shape
|
|
|
|
```json
|
|
{
|
|
"messages": [
|
|
{
|
|
"id": 100,
|
|
"content": "text",
|
|
"authorId": "alice@example.com",
|
|
"authorUsername": "alice",
|
|
"authorAvatarUrl": null,
|
|
"authorHomeServer": "https://home.example.com",
|
|
"channelId": 12,
|
|
"createdAt": "2026-02-20T12:30:00.000",
|
|
"editedAt": null
|
|
}
|
|
],
|
|
"channelId": 12,
|
|
"limit": 50,
|
|
"hasMore": true,
|
|
"nextBefore": 51,
|
|
"nextAfter": 100
|
|
}
|
|
```
|
|
|
|
### Ordering Semantics
|
|
|
|
- no cursor: newest -> oldest (descending by message id)
|
|
- `before=<id>`: older than cursor, still newest -> oldest
|
|
- `after=<id>`: newer than cursor, oldest -> newest (ascending)
|
|
|
|
For client state:
|
|
- `nextBefore`: cursor to request older page
|
|
- `nextAfter`: cursor to request newer page
|
|
|
|
## Recommended Client Flow
|
|
|
|
1. Login/register/join and store JWT.
|
|
2. Initial channel load: `GET /api/messages?channelId=<id>&limit=50`.
|
|
3. Connect `/gateway` with token.
|
|
4. Send `subscribe` for active channel(s).
|
|
5. Merge realtime events into local message list:
|
|
- `message.created`: insert if absent
|
|
- `message.updated`: patch by id
|
|
- `message.deleted`: remove/tombstone by id
|
|
6. Pagination:
|
|
- load older: call `before=<nextBefore>`
|
|
- catch up newer (after reconnect): call `after=<lastSeenMessageId>`
|
|
|
|
## Practical Notes
|
|
|
|
- REST is source of truth; gateway is fanout only.
|
|
- Gateway registry is currently in-memory on server instance (no cross-instance sync yet).
|
|
- Channel permissions are not enforced yet by gateway subscriptions; only channel existence is checked.
|