# Личные сообщения (DM): как это устроено

## Коротко (для быстрого понимания)

Личные сообщения в SHiNE сейчас работают как пара **подписанных клиентом блоков** в формате `SHiNE_dm2`:

- тип `1` — входящее сообщение для собеседника;
- тип `2` — исходящая копия того же сообщения для автора.

Оба блока отправляются вместе одной операцией (`SendMessagePair` / `ReceiveOutcomingMessage`) и либо сохраняются оба, либо не сохраняются вовсе.  
Дальше сервер доставляет их по активным сессиям целевого логина событием `SignedMessageArrived`, а клиент подтверждает доставку на конкретную сессию через `AckSessionDelivery`.

Подтверждение прочтения также идёт парой блоков:

- тип `3` — «прочитано» для исходящего сообщения автора;
- тип `4` — зеркальная копия для второй стороны.

UI чата строится на этих типах: текстовые сообщения (1/2), read-receipt (3/4), непрочитанные, галочки и история.

---

## Подробно

## 1) Общая схема потока

1. Клиент формирует текст сообщения и строит **2 подписанных блока** (`type=1` и `type=2`) с одинаковыми `fromLogin/toLogin/timeMs/nonce`.
2. Клиент отправляет оба блока в одном RPC: `SendMessagePair` (алиас: `ReceiveOutcomingMessage`).
3. Сервер:
   - парсит оба блока;
   - валидирует пару;
   - проверяет существование `from/to` пользователей и подписи;
   - атомарно сохраняет пару в `signed_messages_v2`.
4. Сервер доставляет блоки в активные сессии целевого логина событием `SignedMessageArrived`.
5. Клиент, получив событие, кладёт сообщение в локальный чат и отправляет `AckSessionDelivery(messageKey)`.
6. При открытии чата клиент отправляет read-receipt (пара `type=3/4`) для непрочитанных входящих.

## 2) Формат signed DM-блока (`SHiNE_dm2`)

Префикс: `SHiNE_dm2` (ASCII).

Далее поля (big-endian):

1. `toLoginLen` (`u8`) + `toLogin` (ASCII, 1..60);
2. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, 1..60);
3. `timeMs` (`u64`);
4. `nonce` (`u32`);
5. `messageType` (`u16`);
6. `payloadLen` (`u16`);
7. `payloadBytes` (`1..4096`);
8. `signature` (`64 bytes`, Ed25519).

Ограничения:

- полный пакет: до `8192` байт;
- `messageType` сейчас допустим только `1..4`.

## 3) Типы DM-сообщений

- `1` (`TYPE_INCOMING_TEXT`) — входящий текст для получателя.
- `2` (`TYPE_OUTGOING_COPY`) — исходящая копия в истории автора.
- `3` (`TYPE_READ_INCOMING`) — read-receipt (входящий тип для пары квитанции).
- `4` (`TYPE_READ_OUTGOING_COPY`) — зеркальная копия read-receipt.

Правило пары:

- первый блок должен быть нечётным (`1` или `3`);
- второй должен быть ровно `+1` (`2` или `4`);
- ключевые поля пары совпадают: `toLogin/fromLogin/timeMs/nonce`.

## 4) Ключи сообщений

- `baseKey = from|to|timeMs|nonce`
- `messageKey = baseKey|messageType`

Эти ключи используются:

- для дедупликации;
- для связи read-receipt с исходным сообщением;
- для ACK доставки по сессии.

## 5) RPC и события

## `SendMessagePair` (алиас `ReceiveOutcomingMessage`)

Запрос:

```json
{
  "op": "SendMessagePair",
  "requestId": "req-1",
  "payload": {
    "incomingBlobB64": "<base64 signed block type 1 or 3>",
    "outgoingBlobB64": "<base64 signed block type 2 or 4>"
  }
}
```

Успешный ответ:

```json
{
  "op": "SendMessagePair",
  "requestId": "req-1",
  "status": 200,
  "ok": true,
  "payload": {
    "baseKey": "from|to|time|nonce",
    "incomingKey": "from|to|time|nonce|1",
    "outgoingKey": "from|to|time|nonce|2",
    "deliveredWsSessions": 2,
    "deliveredWebPushSessions": 1
  }
}
```

## `SignedMessageArrived` (server event)

Событие в сессию получателя содержит:

- `messageKey`, `baseKey`;
- `fromLogin`, `toLogin`, `targetLogin`;
- `messageType`, `timeMs`, `nonce`;
- `blobB64`;
- `backlog` (признак догрузки из очереди).

## `AckSessionDelivery`

Запрос:

```json
{
  "op": "AckSessionDelivery",
  "requestId": "ack-1",
  "payload": {
    "messageKey": "from|to|time|nonce|1"
  }
}
```

Ответ: `status=200`, echo `messageKey`.

## 6) Хранение на сервере (SQLite)

Основные таблицы:

1. `signed_messages_v2` — сами DM-блоки типов `1/2/3/4`:
   - `message_key` (PK),
   - `base_key`,
   - `target_login`,
   - `from_login`, `to_login`,
   - `time_ms`, `nonce`, `message_type`,
   - `raw_block`,
   - `source_api`, `origin_session_id`,
   - `receipt_ref_base_key`, `receipt_ref_type`.
2. `signed_message_session_delivery` — доставка по сессиям:
   - составной PK `(message_key, session_id)`,
   - `delivered` (0/1),
   - `delivered_at_ms`, `created_at_ms`.

Примечание: историческая таблица `signed_direct_messages_history` в БД присутствует как legacy-слой, но текущий рабочий поток DM v2 опирается на `signed_messages_v2` + `signed_message_session_delivery`.

## 7) Доставка и backlog

- При сохранении пары сервер пытается сразу доставить в онлайн-сессии.
- Для офлайн/недоступных сессий остаётся pending-запись доставки в таблице `signed_message_session_delivery`.
- При подключении сессии сервер автоматически вызывает `dispatchPendingForSession`:
  - для новой сессии регистрирует все существующие сообщения адресата как «недоставленные»;
  - отправляет **все** pending через WebSocket событием `SignedMessageArrived(backlog=true)`;
  - лимита на количество сообщений нет — передаётся вся история без ограничений.
- Клиент дедублирует входящие через `knownMessageKeys`: если `messageKey` уже есть локально — игнорирует.
- После получения клиент отправляет `AckSessionDelivery`, чтобы отметить `delivered=1` в таблице доставки.

## 8) Read-receipt логика

Когда клиент открывает чат:

1. ищет входящие `messageType=1` без `readReceiptSent`;
2. для каждого отправляет read-receipt как пару `type=3/4`;
3. после успешной отправки помечает `readReceiptSent`.

Сервер для read-receipt хранит ссылку на исходное сообщение:

- `receipt_ref_base_key`;
- `receipt_ref_type`.

Есть уникальность, чтобы не плодить дубликаты receipt на один и тот же `baseKey` для одного `target_login`.

## 9) Логика UI-клиента

### Хранилище сообщений

- In-memory: `state.chats[chatId]` — массив сообщений по каждому диалогу.
- Персистентно: IndexedDB база `shine-ui-messages-v1`, object store `messages`, ключ `messageKey`.
- `chatId` для `type=1` — `fromLogin`, для `type=2` — `toLogin`.

### Жизненный цикл при старте/подключении

1. `hydrateMessagesFromStore()` — читает все сообщения из IndexedDB в `state.chats` (до WebSocket-соединения).
2. После установки WebSocket-сессии сервер присылает backlog (`SignedMessageArrived(backlog=true)`) для всех недоставленных сообщений.
3. Клиент дедублирует через `knownMessageKeys` — уже имеющиеся в IndexedDB игнорируются.
4. Новые сообщения в реальном времени приходят теми же WebSocket-событиями.

### Очистка при выходе и смене пользователя

- При любом логауте (`terminateCurrentSession`) IndexedDB с сообщениями **удаляется полностью**.
- При входе нового пользователя через QR — IndexedDB удаляется явно до вызова `terminateCurrentSession`.
- При входе нового пользователя через логин/пароль — IndexedDB удаляется в `registration-keys-view.js` прямо перед `authorizeSession()`.
- Это гарантирует: при любом способе входа старые сообщения предыдущего пользователя не попадут к следующему.

### UI-поведение

- непрочитанные считаются по `from='in' && unread=true`;
- доставка/прочтение исходящих:
  - `firstTick` — сообщение принято сервером,
  - `secondTick` — пришло подтверждение прочтения;
- при открытии диалога UI автопрокручивает ленту в самый низ;
- после отправки нового сообщения UI сразу прокручивает ленту вниз.

## 10) Синхронизация личных сообщений между серверами

Когда пользователи зарегистрированы на разных серверах SHiNE, серверы должны синхронизировать DM между собой.

### Общий принцип

- Сервер A получает DM-блок, адресованный пользователю на сервере B.
- Сервер A пересылает этот блок серверу B (межсерверный relay).
- Сервер B сохраняет блок и доставляет его в активные сессии получателя.
- Серверы, между которыми идёт синхронизация, задаются списком `sync_servers` в PDA пользователя-сервера.

### Что синхронизируется

- Все DM-блоки типов `1/2` (текстовые сообщения) и `3/4` (read-receipt).
- Синхронизация двусторонняя: оба сервера должны уметь принимать и пересылать блоки.

### Идемпотентность

- Блоки имеют уникальный `message_key` (`from|to|timeMs|nonce|type`).
- Повторная доставка одного и того же блока безопасна — дедупликация происходит по `message_key`.

### Статус реализации

Межсерверная синхронизация DM **пока не реализована**. Текущая версия работает только в рамках одного сервера. Это задача для следующего этапа.

---

## 11) Инварианты (обязательно соблюдать при доработках)

1. Пара блоков (1/2 или 3/4) должна оставаться атомарной.
2. `messageKey`/`baseKey` формат должен быть совместим с текущей логикой дедупликации и receipt.
3. Доставка должна оставаться **по сессиям** с явным `AckSessionDelivery`.
4. Read-receipt не должен отправляться многократно на один и тот же `baseKey`.
5. Любые изменения DM-логики в коде должны сразу отражаться в этом документе.

## 12) Ключевые файлы реализации

- UI:
  - `shine-UI/js/services/auth-service.js`
  - `shine-UI/js/app.js`
  - `shine-UI/js/state.js`
  - `shine-UI/js/pages/chat-view.js`
- Сервер:
  - `shine-server-net-protocol/.../messages/SignedMessageBlock.java`
  - `shine-server-net-protocol/.../messages/SignedMessagesCore.java`
  - `shine-server-net-protocol/.../messages/Net_SendMessagePair_Handler.java`
  - `shine-server-net-protocol/.../messages/SignedMessagesRealtime.java`
  - `shine-server-net-protocol/.../messages/Net_AckSessionDelivery_Handler.java`
- БД:
  - `shine-server-db/src/main/java/shine/db/DatabaseInitializer.java`
  - `shine-server-db/src/main/java/shine/db/dao/SignedMessagesV2DAO.java`
