Files
SHiNE-server/docs/Personal_Messages/Протокол_DM_v1.md

14 KiB

Протокол личных сообщений SHiNE DM v1

1. Статус документа

Это актуальная спецификация логики личных сообщений. Точный байтовый формат контейнера описан в Формат_DM_v1.md, а расписание доставки — в Доставка_и_синхронизация_DM.md.

Формат SHiNE_DM не изменён переходом на один access-сервер.

2. Инварианты

  • каждый DM-контейнер подписан clientKey автора;
  • сервер проверяет формат, пользователей и подпись до сохранения;
  • повторная доставка одной ревизии идемпотентна;
  • более старая ревизия не заменяет новую;
  • у каждого пользователя действует только access_servers[0];
  • дополнительные элементы старой PDA игнорируются без fallback;
  • DM и настройки не реплицируются между access-серверами одного пользователя;
  • sync_servers серверного PDA используются только для синхронизации пользовательских блокчейнов.

3. Типы DM

  • type=1 — входящая копия сообщения для получателя;
  • type=2 — исходящая копия сообщения для отправителя;
  • type=3 — входящий read-receipt;
  • type=4 — исходящая копия read-receipt;
  • type=5 — удаление своего исходящего сообщения;
  • type=6 — удаление входящего сообщения получателем;
  • type=7 — удаление переписки отправителем;
  • type=8 — удаление переписки получателем.

Входящая и исходящая копии являются частями криптографического формата, а не серверной репликацией. Они остаются даже при единственном access-сервере.

4. Создание сообщения

Клиент:

  1. получает актуальные clientKey отправителя и получателя;
  2. шифрует входящую копию на ключ получателя;
  3. шифрует исходящую копию на ключ отправителя;
  4. создаёт два связанных контейнера с общим baseKey;
  5. подписывает оба контейнера clientKey отправителя;
  6. вызывает SendMessagePair на своём access-сервере.

Сервер отправителя атомарно сохраняет пару. Исходящая копия доступна отправителю, а входящая хранится для доставки и повторных попыток.

5. Единственный сервер доступа

Пользовательская PDA по-прежнему содержит массив access_servers для совместимости формата. Действующим считается только первый элемент массива.

Routing-проекция user_access_servers_current содержит не более одной строки на пользователя и строится строго из элемента с ord=1.

Если первый элемент отсутствует, указывает не на server-PDA или server_address пуст, маршрут считается отсутствующим. Второй элемент автоматически не используется.

UI позволяет:

  • увидеть текущий сервер;
  • заменить его другим сервером;
  • записать только один логин в access_servers.

Смена сервера не переносит сообщения и настройки. Новый сервер начинает с пустой локальной истории.

6. Доставка сообщения

6.1. SendMessagePair

Официальный UI отправляет пару только через SendMessagePair.

Сервер:

  1. проверяет оба контейнера и их связь;
  2. сохраняет пару;
  3. создаёт delivery-state;
  4. до ответа UI выполняет первую попытку доставки;
  5. возвращает baseKey, incomingKey, outgoingKey и deliveryState.

ReceiveOutcomingMessage удалён.

6.2. ReceiveIncomingMessage

Эта операция остаётся внутренней server-to-server операцией. Она нужна, когда отправитель и получатель используют разные физические серверы.

Сервер отправителя передаёт только incomingBlobB64 на единственный сервер получателя. Принимающий сервер:

  • разрешает получателя через свою routing-проекцию;
  • заново проверяет пользовательскую подпись;
  • применяет входящую копию идемпотентно;
  • обновляет диалог и realtime-сессии;
  • возвращает успешный ответ только после сохранения.

ReceiveIncomingMessage не реплицирует историю на второй сервер пользователя.

7. Повторная доставка

Состояния:

  • accepted — пара сохранена сервером отправителя;
  • delivered — incoming-копия сохранена сервером получателя;
  • failed — закончилась финальная попытка;
  • read — подтверждается отдельным type=3/4.

Попытки выполняются сразу, через 30 секунд, 5 минут, 25 минут и 1 час. Перед каждой попыткой первый маршрут получателя читается заново.

Peer отправителя не получает полную пару и не опрашивается о состоянии. DmSyncBatch и GetDmDeliveryStatus удалены.

8. Чтение и realtime

GetDirectMessages возвращает историю страницами. UI использует стабильные messageKey/baseKey для дедупликации и отображает более новую ревизию.

dm_dialog_state хранит:

  • последнюю контентную копию;
  • время и ключ последнего сообщения;
  • unreadCount;
  • watermark прочтения.

WebSocket push ускоряет отображение, но после переподключения клиент запрашивает историю у своего единственного access-сервера.

9. Подтверждения прочтения

Read-receipt создаётся как подписанная пара type=3/type=4 и проходит тот же маршрут SendMessagePair → ReceiveIncomingMessage.

Сервер хранит наибольший известный watermark, поэтому переставленные или повторные receipts не уменьшают состояние прочтения.

10. Редактирование

Редактирование сохраняет логический messageKey исходной копии и увеличивает revisionTimeMs. Сервер принимает только более новую валидную ревизию.

Если доставка исходного сообщения ещё не завершилась, следующая попытка использует актуальную сохранённую incoming-копию.

11. Удаление

DeleteMessage принимает type=5/6. DeleteConversation принимает type=7/8.

Удаление является подписанным tombstone. Оно:

  • проверяется как обычный DM-контейнер;
  • применяется идемпотентно;
  • удаляет или блокирует соответствующий локальный контент;
  • доставляется только на единственные access-серверы обеих сторон.

Если сервер уже знает более новый tombstone, старая ревизия контента не восстанавливает удалённую историю.

12. Хранение

Основные таблицы:

  • signed_messages — подписанные DM-контейнеры;
  • dm_dialog_state — материализованный список диалогов;
  • dm_delivery_state — изменяемое состояние сетевой доставки;
  • user_access_servers_current — единственный действующий маршрут пользователя.

Удалены:

  • dm_sync_outbox;
  • dm_sync_peer_state;
  • user_settings_sync_peer_state;
  • synced-флаги репликации.

13. Границы текущей версии

  • межсерверная авторизация ServerHello пока не подписана;
  • sourceServerLogin не является криптографическим доказательством сервера;
  • пользовательская подпись каждого SHiNE_DM проверяется независимо от транспорта;
  • автоматической миграции истории при смене access-сервера нет;
  • резервного fallback-сервера нет.

14. Сохранённые механизмы

Переход на один access-сервер не затрагивает:

  • байтовый формат SHiNE_DM;
  • шифрование входящей и исходящей копий;
  • пользовательские подписи;
  • deliveryState и retry-воркер;
  • read-receipts;
  • синхронизацию пользовательских блокчейнов через server-PDA sync_servers;
  • общий постоянный WSS ServerConnectionPool.

UI списка личных чатов — фильтры и отображение времени (2026-08-28)

  • Заголовок «Чаты» является переключателем фильтра списка.
  • Доступные группы: «Все чаты», «Близкие друзья», «Контакты», «Новые».
  • Фильтрация использует уже существующий признак отношения диалога: close_friend, contact, none; формат DM и серверный протокол при этом не меняются.
  • Для недавних сообщений UI показывает относительное время (секунды, минуты, часы, дни). Для более старых сообщений текущего года используется формат DD.MM, HH:MM, для другого года — DD.MM.YYYY без времени.

Мультипрофильный клиент (UI, 2026-09-03)

  • На одном устройстве клиент может хранить несколько авторизованных профилей, но одновременно использует только один активный runtime/WebSocket для DM.
  • При переключении профиля новая сохранённая сессия сначала проверяется отдельным временным соединением. Текущий профиль не заменяется, если проверка неуспешна.
  • Web Push может быть зарегистрирован для нескольких профилей на одном браузерном push endpoint. Поле toLogin определяет, какому профилю относится событие.
  • При клике по push-сообщению другого сохранённого профиля UI сначала спрашивает подтверждение переключения. Сам клик по системному уведомлению не является read-receipt и не помечает DM прочитанным.
  • Локальный IndexedDB-кэш DM логически разделён по ownerLogin, чтобы сообщения разных сохранённых профилей не смешивались.

UI: видимость пустого диалога после DeleteConversation

DeleteConversation (type=7/8) остаётся техническим tombstone и сам по себе не считается пользовательским сообщением диалога.

Для списка личных чатов действует правило:

  • если после очистки истории у пары нет обычных DM-сообщений и пользователь не находится в contact, friend или close_friend, строка диалога не показывается;
  • если связь contact, friend или close_friend сохраняется, пустой чат может оставаться в списке как чат существующей связи;
  • при удалении чата с friend/close_friend UI должен отдельно предупредить, что одна очистка истории не уберёт строку чата, и при подтверждении снять социальную связь и очистить историю.

Это правило не меняет wire/API-формат DM и не меняет байтовый формат tombstone.