feat(auth,privacy,web): step-by-step login, privacy settings persistence, TOTP QR, and API docs
Some checks failed
CI / test (push) Failing after 22s
Some checks failed
CI / test (push) Failing after 22s
This commit is contained in:
23
docs/README.md
Normal file
23
docs/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# API Documentation
|
||||
|
||||
This folder contains full backend API documentation for `BenyaMessenger`.
|
||||
|
||||
- Main REST reference: [api-reference.md](./api-reference.md)
|
||||
- Realtime/WebSocket reference: [realtime.md](./realtime.md)
|
||||
|
||||
Base URL:
|
||||
|
||||
- Local: `http://localhost:8000`
|
||||
- Prefix for API v1: `/api/v1`
|
||||
- Full base path for REST endpoints: `/api/v1/...`
|
||||
|
||||
Built-in health endpoints:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /health/live`
|
||||
- `GET /health/ready`
|
||||
|
||||
Auth for protected REST endpoints:
|
||||
|
||||
- Header: `Authorization: Bearer <access_token>`
|
||||
|
||||
938
docs/api-reference.md
Normal file
938
docs/api-reference.md
Normal file
@@ -0,0 +1,938 @@
|
||||
# REST API Reference
|
||||
|
||||
## 1. Conventions
|
||||
|
||||
Base path: `/api/v1`
|
||||
|
||||
Authentication:
|
||||
|
||||
- Use JWT access token in header: `Authorization: Bearer <token>`
|
||||
- Login/refresh endpoints return `{ access_token, refresh_token, token_type }`
|
||||
|
||||
Response codes:
|
||||
|
||||
- `200` success
|
||||
- `201` created
|
||||
- `204` success, no body
|
||||
- `400` bad request
|
||||
- `401` unauthorized
|
||||
- `403` forbidden
|
||||
- `404` not found
|
||||
- `409` conflict
|
||||
- `422` validation/business rule error
|
||||
- `429` rate limit
|
||||
- `503` external service unavailable (email/storage/readiness)
|
||||
|
||||
Common error body:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Error text"
|
||||
}
|
||||
```
|
||||
|
||||
For `/health/ready` failure:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": {
|
||||
"status": "not_ready",
|
||||
"db": false,
|
||||
"redis": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Enums
|
||||
|
||||
### ChatType
|
||||
|
||||
- `private`
|
||||
- `group`
|
||||
- `channel`
|
||||
|
||||
### ChatMemberRole
|
||||
|
||||
- `owner`
|
||||
- `admin`
|
||||
- `member`
|
||||
|
||||
### MessageType
|
||||
|
||||
- `text`
|
||||
- `image`
|
||||
- `video`
|
||||
- `audio`
|
||||
- `voice`
|
||||
- `file`
|
||||
- `circle_video`
|
||||
|
||||
### Message status events
|
||||
|
||||
- `message_delivered`
|
||||
- `message_read`
|
||||
|
||||
## 3. Models (request/response)
|
||||
|
||||
## 3.1 Auth
|
||||
|
||||
### RegisterRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"name": "Benya",
|
||||
"username": "benya",
|
||||
"password": "strongpassword"
|
||||
}
|
||||
```
|
||||
|
||||
### LoginRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "strongpassword",
|
||||
"otp_code": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
`otp_code` is optional and used only when 2FA is enabled.
|
||||
|
||||
### TokenResponse
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "jwt",
|
||||
"refresh_token": "jwt",
|
||||
"token_type": "bearer"
|
||||
}
|
||||
```
|
||||
|
||||
### AuthUserResponse
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"email": "user@example.com",
|
||||
"name": "Benya",
|
||||
"username": "benya",
|
||||
"bio": "optional",
|
||||
"avatar_url": "https://...",
|
||||
"email_verified": true,
|
||||
"twofa_enabled": false,
|
||||
"created_at": "2026-03-08T10:00:00Z",
|
||||
"updated_at": "2026-03-08T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.2 Users
|
||||
|
||||
### UserRead
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Benya",
|
||||
"username": "benya",
|
||||
"email": "user@example.com",
|
||||
"avatar_url": "https://...",
|
||||
"bio": "optional",
|
||||
"email_verified": true,
|
||||
"allow_private_messages": true,
|
||||
"twofa_enabled": false,
|
||||
"created_at": "2026-03-08T10:00:00Z",
|
||||
"updated_at": "2026-03-08T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### UserSearchRead
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Other User",
|
||||
"username": "other",
|
||||
"email": "other@example.com",
|
||||
"avatar_url": null
|
||||
}
|
||||
```
|
||||
|
||||
### UserProfileUpdate
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "New Name",
|
||||
"username": "new_username",
|
||||
"bio": "new bio",
|
||||
"avatar_url": "https://...",
|
||||
"allow_private_messages": true
|
||||
}
|
||||
```
|
||||
|
||||
All fields are optional.
|
||||
|
||||
## 3.3 Chats
|
||||
|
||||
### ChatRead
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 10,
|
||||
"public_id": "A1B2C3D4E5F6G7H8J9K0L1M2",
|
||||
"type": "private",
|
||||
"title": null,
|
||||
"display_title": "Other User",
|
||||
"handle": null,
|
||||
"description": null,
|
||||
"is_public": false,
|
||||
"is_saved": false,
|
||||
"archived": false,
|
||||
"pinned": false,
|
||||
"unread_count": 3,
|
||||
"pinned_message_id": null,
|
||||
"members_count": 2,
|
||||
"online_count": 1,
|
||||
"subscribers_count": null,
|
||||
"counterpart_user_id": 2,
|
||||
"counterpart_name": "Other User",
|
||||
"counterpart_username": "other",
|
||||
"counterpart_is_online": true,
|
||||
"counterpart_last_seen_at": "2026-03-08T10:00:00Z",
|
||||
"last_message_text": "Hello",
|
||||
"last_message_type": "text",
|
||||
"last_message_created_at": "2026-03-08T10:01:00Z",
|
||||
"my_role": "member",
|
||||
"created_at": "2026-03-08T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### ChatCreateRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "group",
|
||||
"title": "My Group",
|
||||
"handle": "mygroup",
|
||||
"description": "optional",
|
||||
"is_public": true,
|
||||
"member_ids": [2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `private`: requires exactly 1 target in `member_ids`
|
||||
- `group`/`channel`: require `title`
|
||||
- `private` cannot be public
|
||||
- public chat requires `handle`
|
||||
|
||||
### ChatDetailRead
|
||||
|
||||
`ChatRead + members[]`, where member item is:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": 2,
|
||||
"role": "member",
|
||||
"joined_at": "2026-03-08T09:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.4 Messages
|
||||
|
||||
### MessageRead
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 100,
|
||||
"chat_id": 10,
|
||||
"sender_id": 1,
|
||||
"reply_to_message_id": null,
|
||||
"forwarded_from_message_id": null,
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
"created_at": "2026-03-08T10:02:00Z",
|
||||
"updated_at": "2026-03-08T10:02:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### MessageCreateRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": 10,
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
"client_message_id": "client-msg-0001",
|
||||
"reply_to_message_id": null
|
||||
}
|
||||
```
|
||||
|
||||
### MessageForwardRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"target_chat_id": 20
|
||||
}
|
||||
```
|
||||
|
||||
### MessageForwardBulkRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"target_chat_ids": [20, 21]
|
||||
}
|
||||
```
|
||||
|
||||
### MessageStatusUpdateRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": 10,
|
||||
"message_id": 100,
|
||||
"status": "message_read"
|
||||
}
|
||||
```
|
||||
|
||||
### MessageReactionToggleRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"emoji": "👍"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.5 Media
|
||||
|
||||
### UploadUrlRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"file_name": "photo.jpg",
|
||||
"file_type": "image/jpeg",
|
||||
"file_size": 123456
|
||||
}
|
||||
```
|
||||
|
||||
### UploadUrlResponse
|
||||
|
||||
```json
|
||||
{
|
||||
"upload_url": "https://...signed...",
|
||||
"file_url": "https://.../bucket/uploads/....jpg",
|
||||
"object_key": "uploads/....jpg",
|
||||
"expires_in": 900,
|
||||
"required_headers": {
|
||||
"Content-Type": "image/jpeg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AttachmentCreateRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"message_id": 100,
|
||||
"file_url": "https://.../bucket/uploads/....jpg",
|
||||
"file_type": "image/jpeg",
|
||||
"file_size": 123456
|
||||
}
|
||||
```
|
||||
|
||||
### AttachmentRead
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"message_id": 100,
|
||||
"file_url": "https://...",
|
||||
"file_type": "image/jpeg",
|
||||
"file_size": 123456
|
||||
}
|
||||
```
|
||||
|
||||
### ChatAttachmentRead
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"message_id": 100,
|
||||
"sender_id": 1,
|
||||
"message_type": "image",
|
||||
"message_created_at": "2026-03-08T10:10:00Z",
|
||||
"file_url": "https://...",
|
||||
"file_type": "image/jpeg",
|
||||
"file_size": 123456
|
||||
}
|
||||
```
|
||||
|
||||
## 3.6 Search and notifications
|
||||
|
||||
### GlobalSearchRead
|
||||
|
||||
```json
|
||||
{
|
||||
"users": [],
|
||||
"chats": [],
|
||||
"messages": []
|
||||
}
|
||||
```
|
||||
|
||||
### NotificationRead
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"user_id": 1,
|
||||
"event_type": "message_created",
|
||||
"payload": "{\"chat_id\":10}",
|
||||
"created_at": "2026-03-08T10:15:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Health endpoints
|
||||
|
||||
### GET `/health`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
### GET `/health/live`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
### GET `/health/ready`
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{ "status": "ready", "db": "ok", "redis": "ok" }
|
||||
```
|
||||
|
||||
## 5. Auth endpoints
|
||||
|
||||
### POST `/api/v1/auth/register`
|
||||
|
||||
Body: `RegisterRequest`
|
||||
Response: `201` + `MessageResponse`
|
||||
|
||||
### POST `/api/v1/auth/login`
|
||||
|
||||
Body: `LoginRequest`
|
||||
Response: `200` + `TokenResponse`
|
||||
|
||||
### POST `/api/v1/auth/refresh`
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "refresh_token": "jwt" }
|
||||
```
|
||||
|
||||
Response: `200` + `TokenResponse`
|
||||
|
||||
### POST `/api/v1/auth/verify-email`
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "token": "verification_token" }
|
||||
```
|
||||
|
||||
Response: `200` + `MessageResponse`
|
||||
|
||||
### POST `/api/v1/auth/resend-verification`
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "email": "user@example.com" }
|
||||
```
|
||||
|
||||
Response: `200` + `MessageResponse`
|
||||
|
||||
### POST `/api/v1/auth/request-password-reset`
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "email": "user@example.com" }
|
||||
```
|
||||
|
||||
Response: `200` + `MessageResponse`
|
||||
|
||||
### POST `/api/v1/auth/reset-password`
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "reset_token",
|
||||
"new_password": "newStrongPassword"
|
||||
}
|
||||
```
|
||||
|
||||
Response: `200` + `MessageResponse`
|
||||
|
||||
### GET `/api/v1/auth/me`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `AuthUserResponse`
|
||||
|
||||
### GET `/api/v1/auth/sessions`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `SessionRead[]`
|
||||
|
||||
### DELETE `/api/v1/auth/sessions/{jti}`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### DELETE `/api/v1/auth/sessions`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### POST `/api/v1/auth/2fa/setup`
|
||||
|
||||
Auth required.
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"secret": "BASE32SECRET",
|
||||
"otpauth_url": "otpauth://..."
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/v1/auth/2fa/enable`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "code": "123456" }
|
||||
```
|
||||
|
||||
Response: `200` + `MessageResponse`
|
||||
|
||||
### POST `/api/v1/auth/2fa/disable`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "code": "123456" }
|
||||
```
|
||||
|
||||
Response: `200` + `MessageResponse`
|
||||
|
||||
## 6. Users endpoints
|
||||
|
||||
### GET `/api/v1/users/me`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `UserRead`
|
||||
|
||||
### GET `/api/v1/users/search?query=<text>&limit=20`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `UserSearchRead[]`
|
||||
Note: if query shorter than 2 chars (after trimming optional leading `@`) returns empty list.
|
||||
|
||||
### PUT `/api/v1/users/profile`
|
||||
|
||||
Auth required.
|
||||
Body: `UserProfileUpdate`
|
||||
Response: `200` + `UserRead`
|
||||
|
||||
### GET `/api/v1/users/blocked`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `UserSearchRead[]`
|
||||
|
||||
### GET `/api/v1/users/contacts`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `UserSearchRead[]`
|
||||
|
||||
### POST `/api/v1/users/{user_id}/contacts`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### POST `/api/v1/users/contacts/by-email`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "email": "target@example.com" }
|
||||
```
|
||||
|
||||
Response: `204`
|
||||
|
||||
### DELETE `/api/v1/users/{user_id}/contacts`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### POST `/api/v1/users/{user_id}/block`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### DELETE `/api/v1/users/{user_id}/block`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### GET `/api/v1/users/{user_id}`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `UserRead`
|
||||
|
||||
## 7. Chats endpoints
|
||||
|
||||
### GET `/api/v1/chats`
|
||||
|
||||
Query params:
|
||||
|
||||
- `limit` (default `50`)
|
||||
- `before_id` (optional)
|
||||
- `query` (optional search by title/type)
|
||||
- `archived` (default `false`)
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatRead[]`
|
||||
|
||||
### GET `/api/v1/chats/saved`
|
||||
|
||||
Returns or creates personal saved-messages chat.
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### GET `/api/v1/chats/discover?query=<text>&limit=30`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatDiscoverRead[]`
|
||||
|
||||
### POST `/api/v1/chats`
|
||||
|
||||
Auth required.
|
||||
Body: `ChatCreateRequest`
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/join`
|
||||
|
||||
Join public group/channel.
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### GET `/api/v1/chats/{chat_id}`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatDetailRead`
|
||||
|
||||
### PATCH `/api/v1/chats/{chat_id}/title`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "title": "New title" }
|
||||
```
|
||||
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### GET `/api/v1/chats/{chat_id}/members`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatMemberRead[]`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/members`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "user_id": 123 }
|
||||
```
|
||||
|
||||
Response: `201` + `ChatMemberRead`
|
||||
|
||||
### PATCH `/api/v1/chats/{chat_id}/members/{user_id}/role`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "role": "admin" }
|
||||
```
|
||||
|
||||
Response: `200` + `ChatMemberRead`
|
||||
|
||||
### DELETE `/api/v1/chats/{chat_id}/members/{user_id}`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/leave`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### DELETE `/api/v1/chats/{chat_id}?for_all=false`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/clear`
|
||||
|
||||
Clear chat history for current user.
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/pin`
|
||||
|
||||
Pin chat message.
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "message_id": 100 }
|
||||
```
|
||||
|
||||
or unpin:
|
||||
|
||||
```json
|
||||
{ "message_id": null }
|
||||
```
|
||||
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### GET `/api/v1/chats/{chat_id}/notifications`
|
||||
|
||||
Auth required.
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": 10,
|
||||
"user_id": 1,
|
||||
"muted": false
|
||||
}
|
||||
```
|
||||
|
||||
### PUT `/api/v1/chats/{chat_id}/notifications`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "muted": true }
|
||||
```
|
||||
|
||||
Response: `200` + `ChatNotificationSettingsRead`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/archive`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/unarchive`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/pin-chat`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/unpin-chat`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
### POST `/api/v1/chats/{chat_id}/invite-link`
|
||||
|
||||
Auth required.
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"chat_id": 10,
|
||||
"token": "invite_token",
|
||||
"invite_url": "https://frontend/join?token=invite_token"
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/v1/chats/join-by-invite`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "token": "invite_token" }
|
||||
```
|
||||
|
||||
Response: `200` + `ChatRead`
|
||||
|
||||
## 8. Messages endpoints
|
||||
|
||||
### POST `/api/v1/messages`
|
||||
|
||||
Auth required.
|
||||
Body: `MessageCreateRequest`
|
||||
Response: `201` + `MessageRead`
|
||||
|
||||
### GET `/api/v1/messages/search?query=<text>&chat_id=<id>&limit=50`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `MessageRead[]`
|
||||
|
||||
### GET `/api/v1/messages/{chat_id}?limit=50&before_id=<message_id>`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `MessageRead[]`
|
||||
|
||||
### PUT `/api/v1/messages/{message_id}`
|
||||
|
||||
Auth required.
|
||||
Body:
|
||||
|
||||
```json
|
||||
{ "text": "Edited text" }
|
||||
```
|
||||
|
||||
Response: `200` + `MessageRead`
|
||||
|
||||
### DELETE `/api/v1/messages/{message_id}?for_all=false`
|
||||
|
||||
Auth required.
|
||||
Response: `204`
|
||||
|
||||
### POST `/api/v1/messages/status`
|
||||
|
||||
Auth required.
|
||||
Body: `MessageStatusUpdateRequest`
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"last_delivered_message_id": 123,
|
||||
"last_read_message_id": 120
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/v1/messages/{message_id}/forward`
|
||||
|
||||
Auth required.
|
||||
Body: `MessageForwardRequest`
|
||||
Response: `201` + `MessageRead`
|
||||
|
||||
### POST `/api/v1/messages/{message_id}/forward-bulk`
|
||||
|
||||
Auth required.
|
||||
Body: `MessageForwardBulkRequest`
|
||||
Response: `201` + `MessageRead[]`
|
||||
|
||||
### GET `/api/v1/messages/{message_id}/reactions`
|
||||
|
||||
Auth required.
|
||||
Response:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "emoji": "👍", "count": 3, "reacted": true }
|
||||
]
|
||||
```
|
||||
|
||||
### POST `/api/v1/messages/{message_id}/reactions/toggle`
|
||||
|
||||
Auth required.
|
||||
Body: `MessageReactionToggleRequest`
|
||||
Response: `200` + `MessageReactionRead[]`
|
||||
|
||||
## 9. Media endpoints
|
||||
|
||||
### POST `/api/v1/media/upload-url`
|
||||
|
||||
Auth required.
|
||||
Body: `UploadUrlRequest`
|
||||
Response: `200` + `UploadUrlResponse`
|
||||
|
||||
Validation:
|
||||
|
||||
- Allowed MIME: `image/jpeg`, `image/png`, `image/webp`, `video/mp4`, `video/webm`, `audio/mpeg`, `audio/ogg`, `audio/webm`, `audio/wav`, `application/pdf`, `application/zip`, `text/plain`
|
||||
- Max size: `MAX_UPLOAD_SIZE_BYTES`
|
||||
|
||||
### POST `/api/v1/media/attachments`
|
||||
|
||||
Auth required.
|
||||
Body: `AttachmentCreateRequest`
|
||||
Response: `200` + `AttachmentRead`
|
||||
|
||||
Rules:
|
||||
|
||||
- `file_url` must point to configured S3 bucket endpoint
|
||||
- Only message sender can attach files
|
||||
|
||||
### GET `/api/v1/media/chats/{chat_id}/attachments?limit=100&before_id=<id>`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `ChatAttachmentRead[]`
|
||||
|
||||
## 10. Notifications
|
||||
|
||||
### GET `/api/v1/notifications?limit=50`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `NotificationRead[]`
|
||||
|
||||
## 11. Global search
|
||||
|
||||
### GET `/api/v1/search?query=<text>&users_limit=10&chats_limit=10&messages_limit=10`
|
||||
|
||||
Auth required.
|
||||
Response: `200` + `GlobalSearchRead`
|
||||
|
||||
## 12. Rate limits
|
||||
|
||||
Configured via env vars:
|
||||
|
||||
- `LOGIN_RATE_LIMIT_PER_MINUTE` for `/auth/login`
|
||||
- `REGISTER_RATE_LIMIT_PER_MINUTE` for `/auth/register`
|
||||
- `REFRESH_RATE_LIMIT_PER_MINUTE` for `/auth/refresh`
|
||||
- `RESET_RATE_LIMIT_PER_MINUTE` for reset/resend flows
|
||||
- `MESSAGE_RATE_LIMIT_PER_MINUTE` and `DUPLICATE_MESSAGE_COOLDOWN_SECONDS` for sending messages
|
||||
|
||||
429 response example:
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Rate limit exceeded. Retry in 34 seconds."
|
||||
}
|
||||
```
|
||||
|
||||
## 13. Notes
|
||||
|
||||
- `public_id` is returned for chats and should be used by clients as stable public identifier.
|
||||
- Invite links are generated for group/channel chats.
|
||||
- In channels, only users with sufficient role (owner/admin) can post.
|
||||
- `email` router exists in codebase but has no public REST endpoints yet.
|
||||
|
||||
240
docs/realtime.md
Normal file
240
docs/realtime.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Realtime WebSocket API
|
||||
|
||||
WebSocket endpoint:
|
||||
|
||||
- `GET /api/v1/realtime/ws?token=<access_token>`
|
||||
|
||||
Authentication:
|
||||
|
||||
- Pass a valid **access token** as query parameter `token`.
|
||||
- If token is missing/invalid, server closes with `1008` (policy violation).
|
||||
|
||||
## 1. Message envelope
|
||||
|
||||
All incoming and outgoing events use the same envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "event_name",
|
||||
"payload": {},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
`timestamp` is present in outgoing events from the server.
|
||||
|
||||
## 2. Incoming events (client -> server)
|
||||
|
||||
## 2.1 `ping`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "ping",
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
Server response: `pong`
|
||||
|
||||
## 2.2 `send_message`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "send_message",
|
||||
"payload": {
|
||||
"chat_id": 10,
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
"temp_id": "tmp-1",
|
||||
"client_message_id": "client-msg-1",
|
||||
"reply_to_message_id": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported `type` values:
|
||||
|
||||
- `text`, `image`, `video`, `audio`, `voice`, `file`, `circle_video`
|
||||
|
||||
## 2.3 `typing_start` / `typing_stop`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "typing_start",
|
||||
"payload": {
|
||||
"chat_id": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.4 `message_delivered` / `message_read`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "message_read",
|
||||
"payload": {
|
||||
"chat_id": 10,
|
||||
"message_id": 150
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Outgoing events (server -> client)
|
||||
|
||||
## 3.1 `connect`
|
||||
|
||||
Sent after successful websocket registration:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "connect",
|
||||
"payload": {
|
||||
"connection_id": "uuid"
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.2 `pong`
|
||||
|
||||
Response to `ping`:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "pong",
|
||||
"payload": {},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.3 `receive_message`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "receive_message",
|
||||
"payload": {
|
||||
"chat_id": 10,
|
||||
"message": {
|
||||
"id": 123,
|
||||
"chat_id": 10,
|
||||
"sender_id": 1,
|
||||
"reply_to_message_id": null,
|
||||
"forwarded_from_message_id": null,
|
||||
"type": "text",
|
||||
"text": "Hello",
|
||||
"created_at": "2026-03-08T12:00:00Z",
|
||||
"updated_at": "2026-03-08T12:00:00Z"
|
||||
},
|
||||
"temp_id": "tmp-1",
|
||||
"client_message_id": "client-msg-1",
|
||||
"sender_id": 1
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.4 Typing events
|
||||
|
||||
`typing_start` and `typing_stop`:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "typing_start",
|
||||
"payload": {
|
||||
"chat_id": 10,
|
||||
"user_id": 2
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.5 Delivery/read events
|
||||
|
||||
`message_delivered` and `message_read`:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "message_read",
|
||||
"payload": {
|
||||
"chat_id": 10,
|
||||
"message_id": 150,
|
||||
"user_id": 2,
|
||||
"last_delivered_message_id": 150,
|
||||
"last_read_message_id": 150
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.6 Presence events
|
||||
|
||||
### `user_online`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "user_online",
|
||||
"payload": {
|
||||
"chat_id": 10,
|
||||
"user_id": 2,
|
||||
"is_online": true
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `user_offline`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "user_offline",
|
||||
"payload": {
|
||||
"chat_id": 10,
|
||||
"user_id": 2,
|
||||
"is_online": false,
|
||||
"last_seen_at": "2026-03-08T12:00:00Z"
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.7 `chat_updated`
|
||||
|
||||
Sent when chat metadata/membership/roles/title changes:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "chat_updated",
|
||||
"payload": {
|
||||
"chat_id": 10
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.8 `error`
|
||||
|
||||
Validation/runtime error during WS processing:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "error",
|
||||
"payload": {
|
||||
"detail": "Invalid event payload"
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Disconnect behavior
|
||||
|
||||
- On disconnect, server unregisters connection.
|
||||
- When last connection for a user closes, server marks user offline and sends `user_offline` to related chats.
|
||||
|
||||
## 5. Practical client recommendations
|
||||
|
||||
- Keep one active socket per tab/session.
|
||||
- Send periodic `ping` and expect `pong`.
|
||||
- Reconnect with exponential backoff.
|
||||
- On `chat_updated`, refresh chat metadata via REST (`GET /api/v1/chats` or `GET /api/v1/chats/{chat_id}`).
|
||||
- Use REST message history endpoints for pagination; WS is realtime transport, not history source.
|
||||
|
||||
Reference in New Issue
Block a user