chat/docs/chat-media-handling.md
2026-07-26 11:21:53 +03:30

6.1 KiB

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.

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:

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.
  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/

{
  "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

{
  "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

[
  {
    "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