# Протокол личных сообщений 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-сервера. UI-правило для открытого диалога: разделитель «Новые сообщения» относится только к непрочитанным сообщениям, которые уже существовали до открытия экрана чата. Если новое входящее сообщение приходит, пока этот диалог уже открыт, клиент просто добавляет его в текущий поток, не создавая новый разделитель «Новые сообщения»; затем обычный механизм видимого чтения/receipt отмечает его прочитанным. ## 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. ## 15. Файлы в личных сообщениях Файл не встраивается байтами в `SHiNE_DM`. До отправки DM официальный браузерный клиент: 1. генерирует отдельные случайные `AES-256-GCM` key и IV; 2. шифрует исходный файл локально; 3. вычисляет `fileId = Base58(SHA-256(ciphertext))`; 4. подписанным HTTP `PUT` загружает ciphertext на свой текущий access-сервер; 5. отправляет обычный контентный DM `type=1/2`, plaintext которого начинается с ``. Внутри `` находятся `fileId`, абсолютный URL сервера отправителя, AES-key/IV и исходная метаинформация файла. Так как весь plaintext контентного DM уже шифруется на ключ получателя/отправителя, сервер не получает ключ файла. Получатель после E2EE-расшифровки DM скачивает ciphertext непосредственно с указанного access-сервера отправителя, сверяет `SHA-256`, расшифровывает файл в браузере и сохраняет исходный файл. Отключение функции «Передача файлов» в локальных дополнительных настройках запрещает только отправку новых файлов; ранее полученные `` остаются скачиваемыми. Хранение ciphertext и HTTP-контракт описаны в `docs/API/18_DM_File_Storage_API.md`. ## 16. Дедупликация файлов и пересылка сообщений (2026-09-11) Для HTTP-хранилища DM-файлов действует усиленное правило content-addressed хранения: - перед загрузкой каждого ciphertext-объекта официальный UI делает `HEAD /dm-files/{fileId}`; - если сервер подтверждает объект с тем же `fileId` и размером, `PUT` с байтами повторно не выполняется; - сервер перед ответом на `HEAD`, `GET` и перед `alreadyExists=true` сам пересчитывает `SHA-256` сохранённого объекта и сверяет его с `fileId`; - повреждённый или подменённый объект не считается существующим и не выдаётся клиенту; при следующем корректном `PUT` он может быть записан заново. Пересылка личного сообщения не вводит новый тип DM и не добавляет признак «переслано». UI создаёт обычное новое контентное сообщение `type=1/2` выбранному собеседнику: - для обычного текста переносится отображаемый текст сообщения; - исходный `` не переносится, поэтому новое сообщение не остаётся ответом на сообщение из старого чата; - для сообщения с файлами повторно используются существующие ``-описатели, поэтому ciphertext не шифруется и не загружается повторно; - сервер и получатель видят пересланное сообщение как обычное новое сообщение без служебной пометки об источнике.