# Протокол личных сообщений 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: видимость пустого диалога после DeleteConversation `DeleteConversation` (`type=7/8`) остаётся техническим tombstone и сам по себе не считается пользовательским сообщением диалога. Для списка личных чатов действует правило: - если после очистки истории у пары нет обычных DM-сообщений и пользователь не находится в `contact`, `friend` или `close_friend`, строка диалога не показывается; - если связь `contact`, `friend` или `close_friend` сохраняется, пустой чат может оставаться в списке как чат существующей связи; - при удалении чата с `friend`/`close_friend` UI должен отдельно предупредить, что одна очистка истории не уберёт строку чата, и при подтверждении снять социальную связь и очистить историю. Это правило не меняет wire/API-формат DM и не меняет байтовый формат tombstone.