SHiNE-server/Dev_Docs/Personal_Messages
2026-05-19 21:00:29 +03:00
..
README.md Исправить edit/delete сообщений, упростить вкладки каналов и улучшить автоскролл DM 2026-05-19 21:00:29 +03:00

Личные сообщения (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)

Запрос:

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

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

{
  "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

Запрос:

{
  "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=1fromLogin, для type=2toLogin;
  • непрочитанные считаются по 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