150 lines
6.1 KiB
Markdown
150 lines
6.1 KiB
Markdown
# Sending & receiving media in chat
|
|
|
|
**Backend → frontend handoff.** How image, video, and voice messages move through MinIO and the `/messages/` endpoint. One decision is still open and blocks upload — see [§5](#5-open-question--how-does-the-client-get-write-access-to-minio).
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Service** | gooyal_chat backend |
|
|
| **Endpoint base** | `/api/chats/<chat_uuid>/messages/` |
|
|
| **Status** | text messages ready · media upload path blocked |
|
|
|
|
---
|
|
|
|
## 1. How it fits together
|
|
|
|
The backend never touches file bytes. Media is written straight from the client to MinIO; the chat backend only stores a short text reference to it (relayed through Mattermost) and, on read, signs a temporary download link. Two separate hops, two separate systems:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant C as Client
|
|
participant M as MinIO
|
|
participant B as Chat backend
|
|
participant MM as Mattermost
|
|
|
|
Note over C,M: Upload phase
|
|
C->>M: PUT file bytes to object_key
|
|
M-->>C: 200 OK
|
|
|
|
Note over C,B: Send phase
|
|
C->>B: POST /messages/ { message_type, object_key }
|
|
B->>MM: post "image:object_key" as text
|
|
MM-->>B: post_id
|
|
B-->>C: 201 { post_id, url }
|
|
|
|
Note over C,B: Read phase
|
|
C->>B: GET /messages/
|
|
B->>MM: fetch posts
|
|
B->>M: presign GET for each object_key
|
|
M-->>B: signed URL (60 min TTL)
|
|
B-->>C: 200 [{ post_id, url, ... }]
|
|
```
|
|
|
|
The consequence: the backend cannot validate a file it never receives, and it never caches a download URL — every read re-signs one fresh.
|
|
|
|
---
|
|
|
|
## 2. Sending a media message
|
|
|
|
1. **Client** — pick a unique object key and `PUT` the file to MinIO directly (bucket `chat`). Suggested key shape: `<type>s/<conversation_uuid>/<random-id>-<filename>` — the backend imposes no format, but a conversation-scoped, collision-proof key keeps the bucket sane.
|
|
2. **Client** — enforce type and size limits *before* upload. The backend does not check either — see [§6](#6-what-the-backend-wont-check-for-you).
|
|
3. **Client** — call `POST /messages/` with the `object_key` that was just uploaded, and the matching `message_type`.
|
|
4. **Backend** — stores the message in Mattermost as text `"<type>:<object_key>"` and returns it with a freshly-signed `url` for immediate display.
|
|
|
|
### Request
|
|
|
|
`POST /api/chats/{chat_uuid}/messages/`
|
|
|
|
```json
|
|
{
|
|
"sender_uuid": "9f2c1e2a-...",
|
|
"message_type": "image",
|
|
"object_key": "images/4b1c.../8a21-sunset.jpg"
|
|
}
|
|
```
|
|
|
|
| Field | Type | Rule |
|
|
|---|---|---|
|
|
| `sender_uuid` | UUID | required |
|
|
| `message_type` | enum (`text` \| `image` \| `video` \| `voice`) | optional — defaults to `text` |
|
|
| `text` | string | required only when `message_type` is `text` |
|
|
| `object_key` | string | required when `message_type` is `image`/`video`/`voice` — just checked for non-empty, nothing else |
|
|
|
|
### Response — `201`
|
|
|
|
```json
|
|
{
|
|
"post_id": "prt_9f81...",
|
|
"sender_uuid": "9f2c1e2a-...",
|
|
"message_type": "image",
|
|
"mattermost_message_type": "",
|
|
"text": null,
|
|
"url": "https://minio.internal/chat/images/...?X-Amz-Signature=...",
|
|
"created_at": "2026-07-26T09:14:02Z"
|
|
}
|
|
```
|
|
|
|
A `403 { "detail": "..." }` comes back instead if the conversation has been closed.
|
|
|
|
---
|
|
|
|
## 3. Reading messages
|
|
|
|
`GET /api/chats/{chat_uuid}/messages/?per_page=20&page=0`
|
|
|
|
```json
|
|
[
|
|
{
|
|
"post_id": "prt_9f81...",
|
|
"message_type": "voice",
|
|
"url": "https://minio.internal/chat/voice/...?X-Amz-Signature=...",
|
|
"created_at": "2026-07-26T09:12:40Z"
|
|
}
|
|
]
|
|
```
|
|
|
|
| Param | Default | Notes |
|
|
|---|---|---|
|
|
| `page` | `0` | ignored if `since` is present |
|
|
| `per_page` | `20` | — |
|
|
| `since` | — | Unix ms timestamp; use for polling new messages instead of paging |
|
|
|
|
> **Don't cache the `url` field.** It's signed for 60 minutes (`MINIO_PRESIGN_EXPIRY_SECONDS`) and re-generated on every request — it is never stored. If a link goes stale (e.g. a long-lived chat view), re-fetch the message rather than reusing the old URL.
|
|
|
|
---
|
|
|
|
## 4. Object key convention
|
|
|
|
The backend treats `object_key` as an opaque string — it validates nothing beyond "non-empty" and constructs no path itself. That freedom is also the risk: two clients that pick colliding keys silently overwrite each other's file in MinIO with no error from this API. Recommended shape:
|
|
|
|
```
|
|
images/{conversation_uuid}/{uuid4}-{original_filename}
|
|
video/{conversation_uuid}/{uuid4}-{original_filename}
|
|
voice/{conversation_uuid}/{uuid4}-{original_filename}
|
|
```
|
|
|
|
Bucket is `chat` (`MINIO_BUCKET_CHAT`) in every environment unless ops overrides it.
|
|
|
|
---
|
|
|
|
## 5. Open question — how does the client get write access to MinIO?
|
|
|
|
> **⚠ Blocks implementation.** This backend exposes **no upload endpoint** — no presigned-PUT route, no STS token issuance, nothing. `StorageService` only signs *download* URLs. There is currently no documented way for a client to legitimately write to the `chat` MinIO bucket.
|
|
|
|
Before building the upload step, confirm with backend/infra which of these it will be:
|
|
|
|
- A new `POST /messages/upload-url/`-style endpoint that returns a short-lived presigned **PUT** URL for a given key (mirrors the existing download flow) — most likely path, but doesn't exist yet.
|
|
- Direct client-held MinIO credentials scoped to the bucket (no backend round-trip).
|
|
- An anonymous/public-write bucket policy scoped by key prefix.
|
|
|
|
Don't build against an assumption here — the upload half of this feature can't ship until backend confirms one of the above (or another option) and, if it's the first, implements the endpoint.
|
|
|
|
---
|
|
|
|
## 6. What the backend won't check for you
|
|
|
|
- **File type / size / content.** The only server-side check is "`object_key` is non-empty." Enforce accepted mime types and max size client-side before upload.
|
|
- **That the object actually exists.** Nothing verifies the key was really written to MinIO before the message is sent. A typo'd key sends successfully and just renders as a broken link later.
|
|
- **Auth on the messages endpoint.** `MessageView` currently has no authentication or permission check at all — anyone who can reach the route can post as any `sender_uuid`. Don't build client trust assumptions (e.g. "the server verified this sender") on top of it; treat it as likely to change.
|
|
|
|
---
|
|
*gooyal_chat · backend↔frontend handoff · generated 2026-07-26*
|