SHA256
225 lines
14 KiB
Markdown
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.
|