Files
Messenger/docs/api-reference.md
benya f746e31616
Some checks failed
CI / test (push) Failing after 1m18s
test(contacts): cover blocked relation for add-by-email
2026-03-08 20:23:50 +03:00

1193 lines
21 KiB
Markdown

# 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`
### Realtime chat activity events
- `typing_start`
- `typing_stop`
- `recording_voice_start`
- `recording_voice_stop`
- `recording_video_start`
- `recording_video_stop`
Server behavior: when a user disconnects, active typing/recording indicators are auto-cleared with corresponding `*_stop` events.
## 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",
"recovery_code": "ABCDE-12345"
}
```
`otp_code` is optional and used when 2FA is enabled.
`recovery_code` is optional one-time fallback when 2FA is enabled.
### TokenResponse
```json
{
"access_token": "jwt",
"refresh_token": "jwt",
"token_type": "bearer"
}
```
### SessionRead
```json
{
"jti": "uuid",
"created_at": "2026-03-08T10:00:00Z",
"ip_address": "127.0.0.1",
"user_agent": "Mozilla/5.0 ...",
"current": false,
"token_type": "refresh"
}
```
### AuthUserResponse
```json
{
"id": 1,
"email": "user@example.com",
"name": "Benya",
"username": "benya",
"bio": "optional",
"avatar_url": "https://...",
"email_verified": true,
"twofa_enabled": false,
"allow_private_messages": true,
"privacy_private_messages": "everyone",
"privacy_last_seen": "everyone",
"privacy_avatar": "everyone",
"privacy_group_invites": "everyone",
"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,
"privacy_private_messages": "everyone",
"privacy_last_seen": "everyone",
"privacy_avatar": "everyone",
"privacy_group_invites": "everyone",
"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,
"privacy_private_messages": "contacts",
"privacy_last_seen": "contacts",
"privacy_avatar": "everyone",
"privacy_group_invites": "contacts"
}
```
All fields are optional.
`privacy_private_messages`: `everyone | contacts | nobody`.
## 3.3 Chats
### ChatRead
```json
{
"id": 10,
"public_id": "A1B2C3D4E5F6G7H8J9K0L1M2",
"type": "private",
"title": null,
"avatar_url": null,
"display_title": "Other User",
"handle": null,
"description": null,
"is_public": false,
"is_saved": false,
"archived": false,
"pinned": false,
"unread_count": 3,
"unread_mentions_count": 1,
"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_avatar_url": "https://...",
"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",
"delivery_status": "read",
"attachment_waveform": [4, 7, 10, 9, 6],
"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,
"include_author": true
}
```
### MessageForwardBulkRequest
```json
{
"target_chat_ids": [20, 21],
"include_author": true
}
```
### 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,
"waveform_points": [4, 7, 10, 9, 6]
}
```
### AttachmentRead
```json
{
"id": 1,
"message_id": 100,
"file_url": "https://...",
"file_type": "image/jpeg",
"file_size": 123456,
"waveform_points": [4, 7, 10, 9, 6]
}
```
### 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[]`
Note: list includes refresh sessions and a synthetic current access-token session (`token_type=access`) for stable UI visibility.
### DELETE `/api/v1/auth/sessions/{jti}`
Auth required.
Response: `204`
Behavior: revokes only the specified refresh session token (`jti`); current access token remains valid.
### DELETE `/api/v1/auth/sessions`
Auth required.
Response: `204`
Behavior: revokes all refresh sessions, invalidates all access tokens issued before this request, and force-closes active realtime WebSocket connections for the user.
### POST `/api/v1/auth/2fa/setup`
Auth required.
Response:
```json
{
"secret": "BASE32SECRET",
"otpauth_url": "otpauth://..."
}
```
If 2FA is already enabled for the account, returns `400` (`"2FA is already enabled"`).
### 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`
### POST `/api/v1/auth/2fa/recovery-codes/regenerate`
Auth required.
Body:
```json
{ "code": "123456" }
```
Response:
```json
{
"codes": ["ABCDE-12345", "FGHIJ-67890"]
}
```
Codes are one-time and shown only at generation time.
### GET `/api/v1/auth/2fa/recovery-codes/status`
Auth required.
Response:
```json
{
"remaining_codes": 8
}
```
## 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`
Errors:
- `404` - user with this email was not found.
- `409` - contact cannot be added because a block relation exists.
### 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`
### PATCH `/api/v1/chats/{chat_id}/profile`
Auth required.
Body (all fields optional):
```json
{
"title": "New title",
"description": "optional description",
"avatar_url": "https://..."
}
```
Rules:
- only for `group`/`channel`
- only for `owner`/`admin`
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`
Rules:
- only `owner` can change member roles.
- owner cannot demote self from `owner`.
### DELETE `/api/v1/chats/{chat_id}/members/{user_id}`
Auth required.
Response: `204`
### POST `/api/v1/chats/{chat_id}/bans/{user_id}`
Auth required (`owner/admin` in group/channel).
Response: `204`
Behavior: bans user from chat and removes membership if present.
### DELETE `/api/v1/chats/{chat_id}/bans/{user_id}`
Auth required (`owner/admin` in group/channel).
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`
Behavior:
- `saved messages`: clears personal history only (chat is not removed).
- `channel` + role `owner/admin`: deletes channel for all members.
- `channel` + role `member`: acts as leave channel (removes only current membership).
- `group/private` with `for_all=false`: removes chat for current user only.
- `for_all=true` (where allowed): deletes chat globally.
### 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`
Note: mentions (`@username`) are delivered even when chat is muted.
## 3.7 Privacy enforcement notes
- User profile privacy is enforced server-side:
- `privacy_avatar` controls whether other users receive `avatar_url`.
- `privacy_last_seen` controls whether private-chat counterpart presence fields are visible:
- `counterpart_is_online`
- `counterpart_last_seen_at`
- For `contacts` mode, visibility is granted only when the viewer is in target user's contacts.
- Group/channel invite restrictions are enforced by `privacy_group_invites`:
- Users with `contacts` can be added only by users present in their contacts list.
- Applies to group/channel creation with initial members and admin add-member action.
### 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"
}
```
Rules:
- only `owner/admin` can create invite links for group/channel chats.
### 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[]`
### GET `/api/v1/messages/{message_id}/thread?limit=100`
Auth required.
Returns root message + nested replies (thread subtree, BFS by reply links).
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`
Behavior:
- `for_all=false`:
- in regular chats: hides message only for current user.
- in channels: not allowed (`422`), channel messages are delete-for-all only.
- `for_all=true`:
- private chats: allowed.
- own messages: allowed.
- group/channel messages by others: allowed only for `owner/admin`.
### 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. Realtime WebSocket
### Endpoint
`GET /api/v1/realtime/ws?token=<access_jwt>` (WebSocket upgrade)
Auth:
- token is required in query string
- invalid/expired token: socket closed with policy violation
### Client -> Server events
```json
{ "event": "ping", "payload": {} }
```
```json
{
"event": "send_message",
"payload": {
"chat_id": 10,
"type": "text",
"text": "Hello",
"client_message_id": "uuid",
"reply_to_message_id": null
}
}
```
```json
{ "event": "typing_start", "payload": { "chat_id": 10 } }
```
```json
{ "event": "typing_stop", "payload": { "chat_id": 10 } }
```
```json
{ "event": "recording_voice_start", "payload": { "chat_id": 10 } }
```
```json
{ "event": "recording_voice_stop", "payload": { "chat_id": 10 } }
```
```json
{ "event": "recording_video_start", "payload": { "chat_id": 10 } }
```
```json
{ "event": "recording_video_stop", "payload": { "chat_id": 10 } }
```
```json
{ "event": "message_delivered", "payload": { "chat_id": 10, "message_id": 123 } }
```
```json
{ "event": "message_read", "payload": { "chat_id": 10, "message_id": 123 } }
```
### Server -> Client envelope
```json
{
"event": "receive_message",
"payload": {},
"timestamp": "2026-03-08T10:00:00Z"
}
```
Common server events:
- `connect`, `pong`, `error`
- `receive_message`, `message_updated`, `message_deleted`
- `typing_start`, `typing_stop`
- `recording_voice_start`, `recording_voice_stop`
- `recording_video_start`, `recording_video_stop`
- `message_delivered`, `message_read`
- `user_online`, `user_offline`
- `chat_updated`, `chat_deleted`
Notes:
- on disconnect, server auto-emits activity stop events (`typing_stop`, `recording_*_stop`) for active chat indicators.
- after revoke-all sessions, realtime connections are force-closed; web client should treat this as logout.
## 12. Global search
### GET `/api/v1/search?query=<text>&users_limit=10&chats_limit=10&messages_limit=10`
Auth required.
Response: `200` + `GlobalSearchRead`
## 13. 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."
}
```
## 14. 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.