# Voice & text chat messages — developer guide

This document describes how the unified chat message API handles **text** and **voice** messages: request shapes, validation, responses, push notifications, and where to change behavior in code.

**Production API reference:** see [API_DOCS.md](../API_DOCS.md) → `POST /api/chats/{conversation_id}/messages/`.

---

## Endpoint

| Method | Path | Auth |
|--------|------|------|
| `GET` | `/api/chats/{conversation_id}/messages/` | Participant (shipper or transporter for that chat) |
| `POST` | `/api/chats/{conversation_id}/messages/` | Same |

Implementation: [`chat_messages`](../api/views.py) in `api/views.py`.

---

## Data model

[`Message`](../core/models.py) (`core/models.py`):

| Field | Notes |
|--------|------|
| `message_type` | `TEXT` or `VOICE` (`Message.MessageType`) |
| `text` | Body or caption; may be empty for pure voice |
| `voice_file` | `FileField`, upload path `messages/voice/%Y/%m/%d/` |

Serialization for reads: [`MessageSerializer`](../api/serializers.py) — includes `voice_url` (absolute URL when request context is present).

Creation: [`MessageCreateSerializer`](../api/serializers.py) — fields `text`, `message_type`, `voice_file`, with **`validate()`** enforcing rules below.

---

## Client requests

### Text (`application/json`)

```json
{
  "text": "Non-empty message body.",
  "message_type": "TEXT"
}
```

- `message_type` may be omitted; it defaults to `TEXT`.
- **Validation:** trimmed `text` must be non-empty. Do not send `voice_file` for `TEXT`.

### Voice (`multipart/form-data`)

| Field | Required | Notes |
|--------|----------|--------|
| `message_type` | Yes | Must be `VOICE` |
| `voice_file` | Yes | Audio file (e.g. `.m4a`, AAC) |
| `text` | No | Optional caption |

**Validation:** `VOICE` requires `voice_file`. Do not send voice as base64 inside JSON unless you add a new endpoint; the current API expects multipart file upload.

---

## Successful response (`201`)

JSON envelope: `status_code`, `message`, `error`, `data`.

`data` includes at least:

- `id`, `conversation_id`, `shipment_id`, `trip_id`
- `sender`, `sender_email`
- `text`, `message_type`
- `voice_file` (relative path or empty), `voice_url` (absolute URL for playback, or `null` for text-only)
- `created_at`

Clients should play voice messages using **`voice_url`** from `GET` or from the `POST` response.

---

## Push notification (`new_message`)

After a successful `POST`, the server notifies recipients via [`notify_users`](../api/notify.py).

FCM message shape:

- **`notification`**: `title` = `New Message`, `body` = text preview or voice note line
- **`data`** (all string values):
  - `type`: `New Message`
  - `tag`: JSON string with `conversation_id`, `shipment_id`, `trip_id`, `message_id`, `sender_id`, `chat_message_type`, `event_type`
  - flat copies of those same keys (plus `title` / `body`) for clients that do not parse `tag`

---

## Server and operations

- **Media:** `MEDIA_URL` / `MEDIA_ROOT` in [`Loadboard/settings.py`](../Loadboard/settings.py). In `DEBUG`, Django serves `/media/` via [`Loadboard/urls.py`](../Loadboard/urls.py); in production, serve or proxy the same path.
- **Upload size:** `DATA_UPLOAD_MAX_MEMORY_SIZE` and `FILE_UPLOAD_MAX_MEMORY_SIZE` are set in settings (with env overrides). Align reverse proxy limits (e.g. nginx `client_max_body_size`) with the Django request body cap.
- **HTTPS:** Clients must load `voice_url` over HTTPS in production to avoid mixed-content issues on web.

### Correct `voice_url` host (behind nginx)

API responses build absolute URLs with [`build_absolute_media_url`](../api/media_urls.py):

1. **Recommended:** set **`PUBLIC_BASE_URL`** in `.env` to your public API origin (no trailing slash), e.g. `https://173.249.56.172` or `https://api.example.com`. Then `voice_url` is always `https://…/media/messages/voice/...` regardless of internal `Host`.
2. **Alternative:** leave `PUBLIC_BASE_URL` unset, set **`USE_SECURE_PROXY_SSL_HEADER=true`** if nginx sends `X-Forwarded-Proto: https`, and ensure **`USE_X_FORWARDED_HOST`** (default true) so `request.build_absolute_uri` matches the client-facing host.

Example env hints: [`deploy/.env.example`](../deploy/.env.example).

---

## Automated tests

[`api/tests/test_chat_flow.py`](../api/tests/test_chat_flow.py):

- Multipart voice upload → `201`, `voice_url` present, notification includes `chat_message_type` `VOICE`
- `TEXT` with blank body → `400`
- `VOICE` without file → `400`

Run:

```bash
./.venv/bin/python manage.py test api.tests.test_chat_flow
```

---

## Troubleshooting

### `PermissionError: [Errno 13] Permission denied` on `.../media/messages/voice/...`

The OS user running Django (often **`www-data`** under Gunicorn) must be able to **create and write** under `MEDIA_ROOT` (e.g. `/var/www/html/loadboard/media`).

**One-time fix on the server** (adjust paths as needed):

```bash
sudo mkdir -p /var/www/html/loadboard/media

sudo chown -R www-data:www-data /var/www/html/loadboard/media
sudo chmod -R u+rwX,g+rwX /var/www/html/loadboard/media
```

If the app user is not `www-data`, use that user instead. Restart Gunicorn after fixing ownership.

The API also calls [`ensure_upload_dir`](../api/storage_utils.py) for `messages/voice/YYYY/MM/DD` before saving voice messages so dated folders exist; it cannot fix a root-owned or non-writable `media` tree.

---

## Related optional enhancements (not implemented)

| Idea | Location |
|------|----------|
| `duration_seconds` on `Message` | New field + migration |
| MIME / extension whitelist for `voice_file` | `MessageCreateSerializer` |
| Stronger rate limits on uploads | Middleware / nginx |

---

## Quick reference — files

| Topic | File |
|--------|------|
| View + FCM payload | `api/views.py` → `chat_messages` |
| Create validation | `api/serializers.py` → `MessageCreateSerializer` |
| Model | `core/models.py` → `Message` |
| FCM send | `api/notify.py` → `notify_users`, `send_fcm_data_to_user` |
