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

225 lines
14 KiB
Markdown

# Протокол личных сообщений 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.