13 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. Создание сообщения
Клиент:
- получает актуальные clientKey отправителя и получателя;
- шифрует входящую копию на ключ получателя;
- шифрует исходящую копию на ключ отправителя;
- создаёт два связанных контейнера с общим baseKey;
- подписывает оба контейнера clientKey отправителя;
- вызывает 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.
Сервер:
- проверяет оба контейнера и их связь;
- сохраняет пару;
- создаёт delivery-state;
- до ответа UI выполняет первую попытку доставки;
- возвращает 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_friendUI должен отдельно предупредить, что одна очистка истории не уберёт строку чата, и при подтверждении снять социальную связь и очистить историю.
Это правило не меняет wire/API-формат DM и не меняет байтовый формат tombstone.