SHiNE-server/Dev_Docs/Personal_Messages
AidarKC e3c1cbf1c0 Обновить UI каналов, логаут DM и документацию
- Исправлена вкладка Каналы: стабильные режимы Все/Мои, корректные кнопки и навигация назад.

- Зафиксирована доработка по личным сообщениям: при logout очищается локальная база/кеш DM на устройстве.

- Обновлены AGENTS/CLAUDE и документация Personal_Messages.

- Обновлены версии в VERSION.properties (client 1.2.106, server 1.2.99).
2026-05-31 20:30:31 +04:00
..
README.md Обновить UI каналов, логаут DM и документацию 2026-05-31 20:30:31 +04:00
TODO_доработка_персональных_сообщений_для_агентов.md Добавил гостевой режим, единые shine-ссылки и пометку о нестабильности мнений 2026-05-20 16:14:59 +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-запись доставки в таблице 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=1fromLogin, для type=2toLogin.

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

  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