# Личные сообщения (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": "", "outgoingBlobB64": "" } } ``` Успешный ответ: ```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) Инварианты (обязательно соблюдать при доработках) 1. Пара блоков (1/2 или 3/4) должна оставаться атомарной. 2. `messageKey`/`baseKey` формат должен быть совместим с текущей логикой дедупликации и receipt. 3. Доставка должна оставаться **по сессиям** с явным `AckSessionDelivery`. 4. Read-receipt не должен отправляться многократно на один и тот же `baseKey`. 5. Любые изменения DM-логики в коде должны сразу отражаться в этом документе. ## 11) Ключевые файлы реализации - 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`