# Личные сообщения (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-запись доставки. - При подключении сессии сервер догружает pending (`listPendingForSession`) и шлёт `SignedMessageArrived(backlog=true)`. - После получения клиент должен отправить `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-клиента В UI: - чат хранится в `state.chats[chatId]`; - `chatId` для `type=1` — `fromLogin`, для `type=2` — `toLogin`; - непрочитанные считаются по `from='in' && unread=true`; - доставка/прочтение исходящих: - `firstTick` — сообщение принято в парный поток, - `secondTick` — пришло подтверждение прочтения; - при открытии диалога UI автопрокручивает ленту в самый низ; - после отправки нового сообщения UI сразу автопрокручивает ленту вниз, чтобы новое сообщение было в зоне видимости; - сообщения дополнительно кешируются в IndexedDB (`shine-ui-messages-v1`, store `messages`). ## 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`