SHiNE-server/Dev_Docs/Personal_Messages/README.md

222 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Личные сообщения (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-запись доставки.
- При подключении сессии сервер догружает 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`