SHA256
Убрали старый sync и обновили bundle
Что сделано: вычистили неиспользуемый user-settings sync/DM sync хвост, сохранили сборку, обновили bundle.sh так, чтобы gradle-wrapper.jar всегда попадал в архив. Проверено: compileJava и deploy на t2 (server + UI). Не проверяли: полные интеграционные сценарии, ручные UI-флоу и продовый деплой.
This commit is contained in:
@@ -6,7 +6,8 @@
|
||||
|
||||
- `docs/Personal_Messages/Протокол_DM_v1.md` — логика протокола, роли API, серверное поведение, routing по `access_servers`
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md` — точный бинарный формат контейнера `SHiNE_DM`
|
||||
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md` — состояния доставки, retry-воркер, репликация между двумя access-серверами и UI-статусы
|
||||
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md` — доставка на
|
||||
единственный access-сервер, retry-воркер и UI-статусы
|
||||
- `docs/Personal_Messages/Технические_вставки_DM_v1.md` — формат специальных `<S:...>` вставок внутри plaintext DM после расшифровки
|
||||
|
||||
Исторический устаревший документ сохранён отдельно:
|
||||
|
||||
@@ -1,138 +1,84 @@
|
||||
# Доставка и синхронизация личных сообщений
|
||||
# Доставка личных сообщений
|
||||
|
||||
## 1. Главный принцип
|
||||
## 1. Инвариант
|
||||
|
||||
DM считается доставленным, когда signed-входящую копию сохранил хотя бы один актуальный access-сервер получателя. Доставка на оба сервера не требуется: сервер получателя самостоятельно синхронизирует входящее сообщение со своим вторым сервером.
|
||||
У каждого пользователя действует ровно один сервер доступа: первый элемент
|
||||
access_servers[0] в его Solana PDA.
|
||||
|
||||
Клиентский `status=200` от `SendMessagePair` означает, что собственный сервер сохранил пару. Это отдельный факт от доставки получателю.
|
||||
Остальные элементы старой PDA, если они существуют, не участвуют в
|
||||
маршрутизации и не являются fallback. UI позволяет только заменить текущий
|
||||
сервер другим.
|
||||
|
||||
Формат подписанного контейнера `SHiNE_DM` не меняется. Состояние доставки и флаг синхронизации — изменяемые серверные метаданные.
|
||||
Межсерверной репликации личной переписки между серверами одного пользователя
|
||||
нет. При смене сервера история автоматически не переносится.
|
||||
|
||||
## 2. Идентификаторы
|
||||
## 2. Подписанные копии
|
||||
|
||||
- `baseKey` связывает входящую и исходящую копии одного логического сообщения;
|
||||
- `incomingKey` идентифицирует копию получателя;
|
||||
- `outgoingKey`/`messageKey` идентифицирует копию отправителя и используется для проверки доставки;
|
||||
- дополнительный публичный `eventId` для доставки не создаётся;
|
||||
- `syncId` используется только внутри `DmSyncBatch` как ACK конкретной ревизии, потому что редактирование сохраняет прежний `messageKey`.
|
||||
Формат SHiNE_DM не меняется. Для сообщения клиент создаёт:
|
||||
|
||||
## 3. Состояния доставки
|
||||
- входящую копию для получателя;
|
||||
- исходящую копию для отправителя.
|
||||
|
||||
| Состояние | Смысл | Повторные попытки |
|
||||
|---|---|---|
|
||||
| `accepted` | пара сохранена сервером отправителя, но ни один сервер получателя ещё не подтвердил запись | да |
|
||||
| `delivered` | хотя бы один сервер получателя подтвердил запись | нет |
|
||||
| `failed` | последняя попытка через час завершилась без доставки | никогда |
|
||||
Сервер отправителя принимает пару через SendMessagePair, атомарно сохраняет её
|
||||
и создаёт изменяемое состояние доставки.
|
||||
|
||||
Состояние `delivered` терминальное. Сервер отправителя не пытается отдельно добиться второго ACK получателя.
|
||||
## 3. Основной маршрут
|
||||
|
||||
## 4. Обычная отправка
|
||||
1. UI отправителя вызывает SendMessagePair на своём access-сервере.
|
||||
2. Сервер сохраняет исходящую копию отправителя и входящую копию, необходимую
|
||||
для доставки и повторных попыток.
|
||||
3. Сервер читает единственный маршрут получателя из
|
||||
user_access_servers_current.
|
||||
4. Если это другой физический сервер, он вызывает внутреннюю операцию
|
||||
ReceiveIncomingMessage и передаёт только входящую копию.
|
||||
5. Сервер получателя проверяет пользовательскую подпись, идемпотентно сохраняет
|
||||
входящую копию и уведомляет активные сессии.
|
||||
6. Успешное сохранение означает delivered.
|
||||
|
||||
1. Клиент отправляет `SendMessagePair` на один свой access-сервер.
|
||||
2. Сервер проверяет формат, пользователей, подписи и согласованность пары.
|
||||
3. Сервер атомарно сохраняет входящую и исходящую копии.
|
||||
4. В том же request-процессе сервер читает до двух актуальных маршрутов получателя.
|
||||
5. Оба вызова `ReceiveIncomingMessage` запускаются параллельно.
|
||||
6. Если хотя бы один вызов успешен, устанавливается `delivered`.
|
||||
7. После этой попытки сервер передаёт полную пару своему второму access-серверу старой операцией `ReceiveOutcomingMessage`.
|
||||
8. `SendMessagePair` возвращает существующие ключи и единственное новое поле `deliveryState`.
|
||||
ReceiveIncomingMessage не является синхронизацией двух серверов одного
|
||||
пользователя. Это основная доставка между серверами разных пользователей.
|
||||
|
||||
Успешный повтор уже сохранённого signed-блока считается ACK. Все операции должны быть идемпотентными.
|
||||
## 4. Повторные попытки
|
||||
|
||||
## 5. Расписание повторов
|
||||
Попытки привязаны ко времени первоначального принятия:
|
||||
|
||||
Воркер запускается каждые 5 секунд и выбирает только due-строки по индексу. Он не сканирует всю таблицу сообщений.
|
||||
- сразу;
|
||||
- через 30 секунд;
|
||||
- через 5 минут;
|
||||
- через 25 минут;
|
||||
- через 1 час.
|
||||
|
||||
Попытки привязаны к времени первоначального принятия:
|
||||
Перед каждой попыткой заново читается первый актуальный access-сервер
|
||||
получателя. После финальной неудачи состояние становится failed.
|
||||
|
||||
| Номер | Время от старта | Сначала спросить второй сервер отправителя |
|
||||
|---:|---:|---|
|
||||
| 1 | сразу | нет |
|
||||
| 2 | 30 секунд | нет |
|
||||
| 3 | 5 минут | да |
|
||||
| 4 | 25 минут | да |
|
||||
| 5 | 1 час | да |
|
||||
Второй сервер отправителя не опрашивается, полная пара ему не передаётся.
|
||||
|
||||
Из-за шага воркера повтор может начаться на несколько секунд позже указанного времени. Первая попытка выполняется немедленно и от воркера не зависит.
|
||||
## 5. Состояния
|
||||
|
||||
На 5-й, 25-й и 60-й минутах сервер сначала вызывает у второго сервера отправителя:
|
||||
- accepted — пара сохранена сервером отправителя;
|
||||
- delivered — входящая копия сохранена единственным сервером получателя;
|
||||
- failed — финальная попытка завершилась неудачей;
|
||||
- read-receipt — отдельное подписанное DM-событие.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetDmDeliveryStatus",
|
||||
"payload": {
|
||||
"messageKey": "alice|bob|1774700000123|123456789|2"
|
||||
}
|
||||
}
|
||||
```
|
||||
Подписанные DM-контейнеры остаются криптографическими фактами. Сетевое
|
||||
deliveryState остаётся изменяемой серверной метаданной.
|
||||
|
||||
Ответ:
|
||||
## 6. Удаления
|
||||
|
||||
```json
|
||||
{
|
||||
"messageKey": "alice|bob|1774700000123|123456789|2",
|
||||
"known": true,
|
||||
"delivered": true
|
||||
}
|
||||
```
|
||||
DeleteMessage и DeleteConversation доставляют подписанный tombstone только на
|
||||
единственные access-серверы обеих сторон. Повторное применение идемпотентно.
|
||||
|
||||
Операция read-only. Если peer отвечает `delivered=true`, локальный сервер устанавливает `delivered` и не обращается к серверам получателя. Ошибка или отсутствие операции на старом peer не блокирует собственную попытку.
|
||||
## 7. Удалённая репликация
|
||||
|
||||
Перед каждой попыткой маршруты получателя заново читаются из `user_access_servers_current`. Изменение серверов учитывается только пока сообщение находится в `accepted`. После `delivered` или `failed` состояние больше не открывается.
|
||||
После перехода на один сервер доступа удалены:
|
||||
|
||||
Если последняя проверка и попытка через час не дали ACK, устанавливается `failed`, `next_attempt_at_ms` очищается и сообщение больше никогда автоматически не отправляется.
|
||||
- ReceiveOutcomingMessage;
|
||||
- DmSyncBatch;
|
||||
- GetDmDeliveryStatus;
|
||||
- dm_sync_outbox;
|
||||
- dm_sync_peer_state;
|
||||
- ACK-флаги synced;
|
||||
- периодический DM-backfill между access-серверами.
|
||||
|
||||
## 6. Старая межсерверная доставка
|
||||
|
||||
Форматы существующих операций не расширяются данными о результате доставки:
|
||||
|
||||
- `ReceiveOutcomingMessage` получает прежнюю пару `incomingBlobB64` + `outgoingBlobB64` и необязательный `sourceServerLogin`;
|
||||
- `ReceiveIncomingMessage` получает один `incomingBlobB64` и необязательный `sourceServerLogin`.
|
||||
|
||||
Второй сервер отправителя после получения пары создаёт собственное локальное состояние доставки и самостоятельно пробует маршруты получателя. Серверы обмениваются результатом только через read-only `GetDmDeliveryStatus`.
|
||||
|
||||
## 7. Догоняющая синхронизация двух серверов пользователя
|
||||
|
||||
Для каждого владельца сервер ведёт outbox с флагом `synced`:
|
||||
|
||||
- исходящая пара отправителя — один элемент с двумя blob в порядке incoming/outgoing;
|
||||
- входящая копия получателя — один элемент с одним blob;
|
||||
- read-receipt и tombstone применяются теми же проверенными обработчиками.
|
||||
|
||||
`DmSyncBatch` возвращает только элементы с `synced=false`. Получатель проверяет signed-блоки, сохраняет их идемпотентно и в следующем запросе подтверждает `syncId` через `ackSyncIds`. Источник ставит `synced=true` только после ACK.
|
||||
|
||||
При обрыве соединения неподтверждённый элемент остаётся `synced=false` и безопасно приходит повторно. Элемент, полученный от peer, локально сразу считается синхронизированным, чтобы не образовалась петля.
|
||||
|
||||
`sourceServerLogin` является единственным признаком межсерверного вызова для этих операций: если поле пустое, запрос считается клиентским и его запись не должна сразу переводиться в `synced=true`.
|
||||
|
||||
Синхронизация настроек и DM проходит последовательно через один WS-сеанс: сначала настройки, затем все страницы DM. Если второй сервер был выключен, после включения он сам догружает пропущенные элементы.
|
||||
|
||||
Существующий `MarkAllUserSettingsUnsynced` также сбрасывает DM-флаги. После сброса история повторно передаётся как синхронизация, но старые сообщения получателю заново не отправляются: delivery-состояние создаётся с учётом их возраста и не открывает завершённую часовую очередь.
|
||||
|
||||
## 8. UI
|
||||
|
||||
| Вид | Значение |
|
||||
|---|---|
|
||||
| одна серая галочка | собственный сервер принял сообщение (`accepted`) |
|
||||
| одна светлая галочка | хотя бы один сервер получателя принял сообщение (`delivered`) |
|
||||
| две светлые галочки | пришёл существующий read-receipt |
|
||||
| красный `!` и «Сообщение не доставлено» | окончательное состояние `failed` |
|
||||
|
||||
Read-receipt имеет приоритет над delivery-индикатором: если сообщение прочитано, оно заведомо было доставлено.
|
||||
|
||||
Сервер сообщает поздние изменения через `DmDeliveryStateChanged` с полями `outgoingKey`, `baseKey`, `deliveryState`. При повторном открытии чата то же состояние приходит в `GetDirectMessages`.
|
||||
|
||||
## 9. Нагрузка и отказоустойчивость
|
||||
|
||||
- воркер выбирает только due-записи по частичному индексу;
|
||||
- терминальные строки не попадают в рабочую выборку;
|
||||
- два маршрута первой попытки выполняются параллельно;
|
||||
- ограниченный пул потоков и очередь защищают сервер при всплеске отправок;
|
||||
- сетевой lease и сравнение версии строки предотвращают одновременную обработку одной задачи несколькими worker-потоками;
|
||||
- повторная запись одного signed-блока безопасна;
|
||||
- отсутствие второго сервера отправителя не мешает собственной доставке;
|
||||
- отсутствие обоих серверов получателя завершает задачу через час.
|
||||
|
||||
## 10. Граница доверия
|
||||
|
||||
Межсерверная авторизация пока отложена. `sourceServerLogin` временно принимается на доверии, но каждый контейнер `SHiNE_DM` всё равно проходит проверку пользовательской подписи. `GetDmDeliveryStatus` сообщает только факт локального delivery-state и не изменяет данные.
|
||||
Блокчейн-синхронизация через server-PDA sync_servers остаётся отдельным
|
||||
механизмом и продолжает работать.
|
||||
|
||||
@@ -1,696 +1,199 @@
|
||||
# Личные сообщения (DM) — спецификация v1
|
||||
# Протокол личных сообщений SHiNE DM v1
|
||||
|
||||
## Статус документа
|
||||
## 1. Статус документа
|
||||
|
||||
Этот файл — актуальная логическая спецификация DM-протокола SHiNE v1.
|
||||
Это актуальная спецификация логики личных сообщений. Точный байтовый формат
|
||||
контейнера описан в Формат_DM_v1.md, а расписание доставки — в
|
||||
Доставка_и_синхронизация_DM.md.
|
||||
|
||||
Важно:
|
||||
Формат SHiNE_DM не изменён переходом на один access-сервер.
|
||||
|
||||
- это актуальная реализованная спецификация;
|
||||
- код и документы по DM должны изменяться синхронно;
|
||||
- при любых будущих изменениях DM сначала обновляется эта спецификация и соседний документ формата.
|
||||
## 2. Инварианты
|
||||
|
||||
Документ фиксирует:
|
||||
- каждый DM-контейнер подписан clientKey автора;
|
||||
- сервер проверяет формат, пользователей и подпись до сохранения;
|
||||
- повторная доставка одной ревизии идемпотентна;
|
||||
- более старая ревизия не заменяет новую;
|
||||
- у каждого пользователя действует только access_servers[0];
|
||||
- дополнительные элементы старой PDA игнорируются без fallback;
|
||||
- DM и настройки не реплицируются между access-серверами одного пользователя;
|
||||
- sync_servers серверного PDA используются только для синхронизации
|
||||
пользовательских блокчейнов.
|
||||
|
||||
- модель DM и смысл типов сообщений;
|
||||
- правила редактирования, перешифровки и удаления;
|
||||
- роли существующих API-методов;
|
||||
- правила межсерверной маршрутизации через `access_servers`;
|
||||
- общее поведение сервера и БД.
|
||||
## 3. Типы DM
|
||||
|
||||
Точный байтовый формат контейнера вынесен отдельно:
|
||||
- type=1 — входящая копия сообщения для получателя;
|
||||
- type=2 — исходящая копия сообщения для отправителя;
|
||||
- type=3 — входящий read-receipt;
|
||||
- type=4 — исходящая копия read-receipt;
|
||||
- type=5 — удаление своего исходящего сообщения;
|
||||
- type=6 — удаление входящего сообщения получателем;
|
||||
- type=7 — удаление переписки отправителем;
|
||||
- type=8 — удаление переписки получателем.
|
||||
|
||||
- `docs/Personal_Messages/Формат_DM_v1.md`
|
||||
Входящая и исходящая копии являются частями криптографического формата, а не
|
||||
серверной репликацией. Они остаются даже при единственном access-сервере.
|
||||
|
||||
Формат клиентских технических вставок внутри plaintext вынесен отдельно:
|
||||
## 4. Создание сообщения
|
||||
|
||||
- `docs/Personal_Messages/Технические_вставки_DM_v1.md`
|
||||
Клиент:
|
||||
|
||||
Изменяемое состояние доставки, retry-расписание и репликация между двумя access-серверами подробно описаны отдельно:
|
||||
1. получает актуальные clientKey отправителя и получателя;
|
||||
2. шифрует входящую копию на ключ получателя;
|
||||
3. шифрует исходящую копию на ключ отправителя;
|
||||
4. создаёт два связанных контейнера с общим baseKey;
|
||||
5. подписывает оба контейнера clientKey отправителя;
|
||||
6. вызывает SendMessagePair на своём access-сервере.
|
||||
|
||||
- `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`
|
||||
Сервер отправителя атомарно сохраняет пару. Исходящая копия доступна отправителю,
|
||||
а входящая хранится для доставки и повторных попыток.
|
||||
|
||||
Устаревшая предыдущая версия сохранена отдельно:
|
||||
## 5. Единственный сервер доступа
|
||||
|
||||
- `docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md`
|
||||
Пользовательская PDA по-прежнему содержит массив access_servers для
|
||||
совместимости формата. Действующим считается только первый элемент массива.
|
||||
|
||||
## 1. Основная модель
|
||||
Routing-проекция user_access_servers_current содержит не более одной строки на
|
||||
пользователя и строится строго из элемента с ord=1.
|
||||
|
||||
Личное сообщение в SHiNE хранится как две логические копии одного сообщения:
|
||||
Если первый элемент отсутствует, указывает не на server-PDA или server_address
|
||||
пуст, маршрут считается отсутствующим. Второй элемент автоматически не
|
||||
используется.
|
||||
|
||||
- `type=1` — входящее сообщение для получателя;
|
||||
- `type=2` — исходящая копия сообщения для отправителя.
|
||||
UI позволяет:
|
||||
|
||||
Обе копии имеют общий логический идентификатор:
|
||||
- увидеть текущий сервер;
|
||||
- заменить его другим сервером;
|
||||
- записать только один логин в access_servers.
|
||||
|
||||
- `baseKey = fromLogin|toLogin|timeMs|nonce`
|
||||
Смена сервера не переносит сообщения и настройки. Новый сервер начинает с
|
||||
пустой локальной истории.
|
||||
|
||||
Идентификатор конкретной копии:
|
||||
## 6. Доставка сообщения
|
||||
|
||||
- `messageKey = baseKey|messageType`
|
||||
### 6.1. SendMessagePair
|
||||
|
||||
Поля `timeMs` и `nonce` не меняются никогда.
|
||||
Официальный UI отправляет пару только через SendMessagePair.
|
||||
|
||||
`nonce` обязателен, потому что одного `timeMs` недостаточно для гарантированной уникальности.
|
||||
Сервер:
|
||||
|
||||
## 2. Ключи и шифрование
|
||||
1. проверяет оба контейнера и их связь;
|
||||
2. сохраняет пару;
|
||||
3. создаёт delivery-state;
|
||||
4. до ответа UI выполняет первую попытку доставки;
|
||||
5. возвращает baseKey, incomingKey, outgoingKey и deliveryState.
|
||||
|
||||
### 2.1. Общий принцип
|
||||
ReceiveOutcomingMessage удалён.
|
||||
|
||||
У пользователя в PDA публикуется один публичный `clientKey` в формате `Ed25519`.
|
||||
### 6.2. ReceiveIncomingMessage
|
||||
|
||||
Из того же ключевого материала стандартным способом выводится ключ `X25519` для E2EE-шифрования DM.
|
||||
Эта операция остаётся внутренней server-to-server операцией. Она нужна, когда
|
||||
отправитель и получатель используют разные физические серверы.
|
||||
|
||||
Отдельный публичный `dmEncKey` в текущей архитектуре не хранится.
|
||||
Сервер отправителя передаёт только incomingBlobB64 на единственный сервер
|
||||
получателя. Принимающий сервер:
|
||||
|
||||
Подробности стандартного преобразования:
|
||||
- разрешает получателя через свою routing-проекцию;
|
||||
- заново проверяет пользовательскую подпись;
|
||||
- применяет входящую копию идемпотентно;
|
||||
- обновляет диалог и realtime-сессии;
|
||||
- возвращает успешный ответ только после сохранения.
|
||||
|
||||
- `docs/Протоколы/Преобразование_ED25519_в_X25519.md`
|
||||
ReceiveIncomingMessage не реплицирует историю на второй сервер пользователя.
|
||||
|
||||
### 2.2. Правило шифрования копий
|
||||
## 7. Повторная доставка
|
||||
|
||||
- `type=1` шифруется на ключ получателя;
|
||||
- `type=2` шифруется на ключ отправителя.
|
||||
Состояния:
|
||||
|
||||
Следствия:
|
||||
- accepted — пара сохранена сервером отправителя;
|
||||
- delivered — incoming-копия сохранена сервером получателя;
|
||||
- failed — закончилась финальная попытка;
|
||||
- read — подтверждается отдельным type=3/4.
|
||||
|
||||
- входящую копию может прочитать только получатель;
|
||||
- исходящую копию может прочитать только отправитель;
|
||||
- сервер не должен расшифровывать DM;
|
||||
- ciphertext у `type=1` и `type=2` обычно разный и не обязан совпадать побайтно.
|
||||
Попытки выполняются сразу, через 30 секунд, 5 минут, 25 минут и 1 час.
|
||||
Перед каждой попыткой первый маршрут получателя читается заново.
|
||||
|
||||
### 2.3. Что именно знает сервер о `body`
|
||||
Peer отправителя не получает полную пару и не опрашивается о состоянии.
|
||||
DmSyncBatch и GetDmDeliveryStatus удалены.
|
||||
|
||||
Для контентных DM (`type=1/2`) сервер в v1 должен рассматривать `body` как opaque blob:
|
||||
## 8. Чтение и realtime
|
||||
|
||||
- сервер проверяет внешний контейнер `SHiNE_DM`;
|
||||
- сервер проверяет подпись;
|
||||
- сервер проверяет служебные поля envelope;
|
||||
- сервер проверяет только то, что `bodyLen > 0` и не превышает допустимый лимит;
|
||||
- сервер не обязан разбирать внутренний crypto-контейнер `body`;
|
||||
- сервер не обязан знать конкретный алгоритм шифрования `body`.
|
||||
GetDirectMessages возвращает историю страницами. UI использует стабильные
|
||||
messageKey/baseKey для дедупликации и отображает более новую ревизию.
|
||||
|
||||
Это позволяет альтернативным клиентам использовать совместимый внешний DM-envelope при собственном формате зашифрованного payload внутри `body`.
|
||||
dm_dialog_state хранит:
|
||||
|
||||
Официальный UI SHiNE при приёме такого сообщения:
|
||||
- последнюю контентную копию;
|
||||
- время и ключ последнего сообщения;
|
||||
- unreadCount;
|
||||
- watermark прочтения.
|
||||
|
||||
- пытается расшифровать знакомый формат `EncryptedBody_v1_0`;
|
||||
- показывает текст сообщения при успешной расшифровке;
|
||||
- если внутренний формат не поддерживается, показывает `Формат сообщения не поддерживается`;
|
||||
- если формат понятен, но расшифровка не удалась, показывает `Не удалось расшифровать сообщение`;
|
||||
- если `body` повреждён или структурно битый, показывает `Сообщение повреждено`.
|
||||
WebSocket push ускоряет отображение, но после переподключения клиент запрашивает
|
||||
историю у своего единственного access-сервера.
|
||||
|
||||
После успешной расшифровки plaintext может дополнительно содержать специальные клиентские вставки `<S:...>` в начале текста. Legacy-вставки `<SHiNE:...>` также продолжают поддерживаться при чтении.
|
||||
Эти вставки относятся уже к уровню UI/plaintext, а не к уровню серверного DM-envelope.
|
||||
## 9. Подтверждения прочтения
|
||||
|
||||
### 2.4. Источник истины по пользователю
|
||||
Read-receipt создаётся как подписанная пара type=3/type=4 и проходит тот же
|
||||
маршрут SendMessagePair → ReceiveIncomingMessage.
|
||||
|
||||
Для DM-проверки сервер использует:
|
||||
Сервер хранит наибольший известный watermark, поэтому переставленные или
|
||||
повторные receipts не уменьшают состояние прочтения.
|
||||
|
||||
- локальную runtime-проекцию пользователей как кэш;
|
||||
- Solana PDA как источник истины по `clientKey` и `access_servers`.
|
||||
## 10. Редактирование
|
||||
|
||||
Если нужного пользователя нет локально, сервер обязан попытаться lazy-import из Solana PDA:
|
||||
Редактирование сохраняет логический messageKey исходной копии и увеличивает
|
||||
revisionTimeMs. Сервер принимает только более новую валидную ревизию.
|
||||
|
||||
- в `GetUser`;
|
||||
- при серверной верификации DM-подписи;
|
||||
- перед межсерверной маршрутизацией через `access_servers`.
|
||||
Если доставка исходного сообщения ещё не завершилась, следующая попытка
|
||||
использует актуальную сохранённую incoming-копию.
|
||||
|
||||
## 3. Типы сообщений
|
||||
## 11. Удаление
|
||||
|
||||
В версии v1 используются следующие `messageType`:
|
||||
DeleteMessage принимает type=5/6. DeleteConversation принимает type=7/8.
|
||||
|
||||
- `1` — входящее сообщение;
|
||||
- `2` — исходящая копия сообщения;
|
||||
- `3` — входящее подтверждение прочтения;
|
||||
- `4` — исходящая копия подтверждения прочтения;
|
||||
- `5` — сообщение удалено отправителем;
|
||||
- `6` — сообщение удалено получателем;
|
||||
- `7` — переписка удалена отправителем;
|
||||
- `8` — переписка удалена получателем.
|
||||
Удаление является подписанным tombstone. Оно:
|
||||
|
||||
Типы `1/2` — это обычные контентные DM, в том числе их последующие ревизии.
|
||||
- проверяется как обычный DM-контейнер;
|
||||
- применяется идемпотентно;
|
||||
- удаляет или блокирует соответствующий локальный контент;
|
||||
- доставляется только на единственные access-серверы обеих сторон.
|
||||
|
||||
Типы `3/4` — служебные подтверждения прочтения.
|
||||
Если сервер уже знает более новый tombstone, старая ревизия контента не
|
||||
восстанавливает удалённую историю.
|
||||
|
||||
Типы `5/6/7/8` — служебные события удаления без зашифрованного тела.
|
||||
## 12. Хранение
|
||||
|
||||
## 4. Базовые операции
|
||||
Основные таблицы:
|
||||
|
||||
### 4.1. Создание нового сообщения
|
||||
- signed_messages — подписанные DM-контейнеры;
|
||||
- dm_dialog_state — материализованный список диалогов;
|
||||
- dm_delivery_state — изменяемое состояние сетевой доставки;
|
||||
- user_access_servers_current — единственный действующий маршрут пользователя.
|
||||
|
||||
Новое сообщение отправляется парой блоков через:
|
||||
Удалены:
|
||||
|
||||
- `SendMessagePair`
|
||||
- `ReceiveOutcomingMessage` как алиас
|
||||
- dm_sync_outbox;
|
||||
- dm_sync_peer_state;
|
||||
- user_settings_sync_peer_state;
|
||||
- synced-флаги репликации.
|
||||
|
||||
Пару создаёт автор сообщения.
|
||||
## 13. Границы текущей версии
|
||||
|
||||
Пара содержит:
|
||||
- межсерверная авторизация ServerHello пока не подписана;
|
||||
- sourceServerLogin не является криптографическим доказательством сервера;
|
||||
- пользовательская подпись каждого SHiNE_DM проверяется независимо от
|
||||
транспорта;
|
||||
- автоматической миграции истории при смене access-сервера нет;
|
||||
- резервного fallback-сервера нет.
|
||||
|
||||
- одну входящую копию `type=1`;
|
||||
- одну исходящую копию `type=2`;
|
||||
- одинаковые `fromLogin`, `toLogin`, `timeMs`, `nonce`;
|
||||
- одинаковый логический plaintext;
|
||||
- разные ciphertext для разных владельцев копий.
|
||||
## 14. Сохранённые механизмы
|
||||
|
||||
### 4.2. Редактирование сообщения
|
||||
Переход на один access-сервер не затрагивает:
|
||||
|
||||
Редактирование общего текста делает автор сообщения.
|
||||
|
||||
При редактировании:
|
||||
|
||||
- `baseKey` остаётся тем же;
|
||||
- `messageType` остаётся тем же;
|
||||
- `revisionTimeMs` увеличивается;
|
||||
- автор заново шифрует обе копии и снова отправляет пару через `SendMessagePair`.
|
||||
|
||||
Если `revisionTimeMs = 0`, это исходная версия.
|
||||
|
||||
Если `revisionTimeMs > 0`, это новая ревизия сообщения.
|
||||
|
||||
### 4.3. Перешифровка
|
||||
|
||||
Перешифровка нужна для будущего сценария полной или частичной перепаковки истории сообщений при сохранении тех же логических идентификаторов сообщений.
|
||||
|
||||
При перешифровке:
|
||||
|
||||
- `baseKey` остаётся прежним;
|
||||
- `messageKey` остаётся прежним;
|
||||
- plaintext может остаться тем же;
|
||||
- ciphertext меняется;
|
||||
- `reencryptedAtMs` получает ненулевое значение;
|
||||
- `revisionTimeMs` может остаться прежним, в том числе нулевым, если менялось только шифрование без редактирования текста;
|
||||
- сервер при выборе актуальной версии обязан учитывать обе метки времени.
|
||||
|
||||
Если сообщение уже удалено одним из типов `5/6`, его перешифровывать больше нельзя.
|
||||
|
||||
Если переписка уже удалена типом `7/8`, более старые сообщения этой пары тоже перешифровывать нельзя.
|
||||
|
||||
### 4.4. Приём входящей копии
|
||||
|
||||
Для server-to-server доставки одной входящей копии используется:
|
||||
|
||||
- `ReceiveIncomingMessage`
|
||||
|
||||
Этот метод должен принимать:
|
||||
|
||||
- новые входящие сообщения;
|
||||
- входящие обновления/редактирования;
|
||||
- будущие входящие перешифрованные копии.
|
||||
|
||||
## 5. Удаление одного сообщения
|
||||
|
||||
### 5.1. Общая логика
|
||||
|
||||
Удаление одного сообщения в v1 всегда глобальное у обеих сторон.
|
||||
|
||||
Локального удаления только у себя в этой версии протокола не вводится.
|
||||
|
||||
### 5.2. Типы удаления
|
||||
|
||||
- `type=5` — сообщение удалено отправителем;
|
||||
- `type=6` — сообщение удалено получателем.
|
||||
|
||||
Удаляющее сообщение:
|
||||
|
||||
- не содержит зашифрованного тела;
|
||||
- хранится в БД как tombstone;
|
||||
- терминально закрывает это сообщение;
|
||||
- не даёт больше принять никакую более позднюю содержательную версию этого же `messageKey`.
|
||||
|
||||
### 5.3. Метод удаления
|
||||
|
||||
Для удаления одного сообщения нужен отдельный метод, условно:
|
||||
|
||||
- `DeleteMessage`
|
||||
|
||||
Если сервер впервые получает валидное удаляющее сообщение:
|
||||
|
||||
- сохраняет tombstone в БД;
|
||||
- удаляет или замещает прежнюю версию сообщения tombstone-записью;
|
||||
- распространяет это же удаление на серверы доступа обеих сторон;
|
||||
- не принимает в будущем попытки "оживить" это сообщение.
|
||||
|
||||
## 6. Удаление всей переписки
|
||||
|
||||
### 6.1. Отдельное служебное сообщение
|
||||
|
||||
Удаление всей переписки между двумя пользователями — это отдельное служебное сообщение без ciphertext.
|
||||
|
||||
Типы:
|
||||
|
||||
- `type=7` — переписка удалена отправителем;
|
||||
- `type=8` — переписка удалена получателем.
|
||||
|
||||
### 6.2. Граница удаления
|
||||
|
||||
Границей удаления считается:
|
||||
|
||||
- `timeMs` самого служебного сообщения удаления переписки
|
||||
|
||||
Отдельное `deleteBeforeTimeMs` в этой версии не вводится.
|
||||
|
||||
### 6.3. Правило применения
|
||||
|
||||
Если сервер получает такое сообщение впервые:
|
||||
|
||||
- сохраняет его в БД как tombstone переписки;
|
||||
- удаляет из БД все сообщения этой пары пользователей с `timeMs` меньше времени служебного сообщения;
|
||||
- больше не принимает новые или повторно доставленные сообщения с `timeMs` раньше этой границы;
|
||||
- распространяет это же сообщение удаления переписки на серверы доступа обеих сторон.
|
||||
|
||||
### 6.4. Поведение при позднем старом сообщении
|
||||
|
||||
Если после удаления переписки приходит старое сообщение, у которого:
|
||||
|
||||
- `timeMs < deleteConversationMessage.timeMs`
|
||||
|
||||
то сервер:
|
||||
|
||||
- не принимает это сообщение;
|
||||
- распространяет уже известный tombstone удаления переписки на серверы доступа обеих сторон;
|
||||
- ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки.
|
||||
|
||||
### 6.5. Будущие новые сообщения
|
||||
|
||||
После удаления всей переписки новые сообщения между этими же пользователями разрешены, если:
|
||||
|
||||
- их `timeMs` больше времени служебного сообщения удаления переписки.
|
||||
|
||||
## 7. Серверы и маршрутизация
|
||||
|
||||
### 7.1. `access_servers`
|
||||
|
||||
Для обычного пользователя список серверов доставки и доступа задаётся через:
|
||||
|
||||
- `access_servers`
|
||||
|
||||
Это:
|
||||
|
||||
- сервера доступа пользователя;
|
||||
- сервера relay;
|
||||
- сервера, через которые пользователь получает личные сообщения и другие пользовательские операции.
|
||||
|
||||
### 7.2. `sync_servers`
|
||||
|
||||
`sync_servers` относятся не к обычной пользовательской маршрутизации DM, а к server-to-server партнёрству серверного узла.
|
||||
|
||||
Они используются для:
|
||||
|
||||
- синхронизации серверных данных;
|
||||
- синхронизации пользовательских блокчейнов SHiNE;
|
||||
- общей межсерверной координации.
|
||||
|
||||
`sync_servers` не являются списком пользовательских серверов доставки DM.
|
||||
|
||||
### 7.3. Два сервера у отправителя и получателя
|
||||
|
||||
Актуальный runtime исходит из ограничения:
|
||||
|
||||
- у одного пользователя не более двух `access_servers`;
|
||||
- следовательно, у локального сервера есть не более одного peer для репликации данных пользователя.
|
||||
|
||||
Протокол поддерживает ситуацию, когда:
|
||||
|
||||
- у отправителя один или два `access_servers`;
|
||||
- у получателя один или два `access_servers`;
|
||||
- часть серверов у сторон совпадает;
|
||||
- часть серверов уникальна.
|
||||
|
||||
Из этого следуют требования:
|
||||
|
||||
- все DM-операции должны быть идемпотентны;
|
||||
- повторное получение уже известного события не должно ломать состояние;
|
||||
- дубль tombstone должен быть безопасен;
|
||||
- сервер не должен "оживлять" более старую версию сообщения после уже принятого tombstone.
|
||||
|
||||
## 8. Методы и их роли
|
||||
|
||||
### 8.1. Существующие методы, которые сохраняются
|
||||
|
||||
- `SendMessagePair`
|
||||
- `ReceiveOutcomingMessage`
|
||||
- `ReceiveIncomingMessage`
|
||||
- `DeleteMessage`
|
||||
- `DeleteConversation`
|
||||
|
||||
Их роли в v1:
|
||||
|
||||
- `SendMessagePair` — новая пара сообщений и редактирование старой пары автором;
|
||||
- `ReceiveIncomingMessage` — приём одной входящей копии, входящих редактирований и входящего read-receipt;
|
||||
- `ReceiveOutcomingMessage` — алиас `SendMessagePair`.
|
||||
|
||||
### 8.2. Межсерверная синхронизация
|
||||
|
||||
- `ReceiveOutcomingMessage` — прежняя полная пара второго access-сервера отправителя;
|
||||
- `ReceiveIncomingMessage` — прежняя одиночная входящая копия;
|
||||
- `DmSyncBatch` — pull событий с `synced=false` и ACK сохранённой предыдущей страницы;
|
||||
- `GetDmDeliveryStatus` — единственная новая read-only проверка доставки по существующему `messageKey`.
|
||||
|
||||
Delivery-state между серверами не передаётся и не объединяется.
|
||||
|
||||
### 8.3. Серверный слой диалогов
|
||||
|
||||
Помимо хранения самих DM-сообщений сервер поддерживает материализованный слой состояния диалогов:
|
||||
|
||||
- отдельная запись на пару `owner_login` + `peer_login`;
|
||||
- `relation_flag` со значениями `close_friend`, `contact`, `none`;
|
||||
- `last_message_blob_b64` как последний контентный signed DM block в base64;
|
||||
- `last_message_time_ms`;
|
||||
- `unread_count`;
|
||||
- `last_read_receipt_time_ms` как watermark последнего подтверждения прочтения.
|
||||
|
||||
Ключевые правила:
|
||||
|
||||
- `close_friend` всегда имеет приоритет над `contact`;
|
||||
- если `read receipt` приходит не по порядку, сервер хранит наибольший watermark и не откатывает состояние назад;
|
||||
- `unread_count` пересчитывается сервером по сообщениям диалога с учётом watermark и `read_at_ms`;
|
||||
- старые исторические данные восстанавливаются из `signed_messages` при инициализации/миграции;
|
||||
- UI не должен собирать inbox только из локального кеша, когда ему доступен серверный список диалогов.
|
||||
|
||||
## 9. Правила валидации и применения
|
||||
|
||||
### 9.1. Общее правило по ревизиям
|
||||
|
||||
Для одного и того же контентного сообщения сервер сравнивает пару:
|
||||
|
||||
- `revisionTimeMs`;
|
||||
- `reencryptedAtMs`.
|
||||
|
||||
Правило:
|
||||
|
||||
- если новый `revisionTimeMs` больше сохранённого, сообщение применяется;
|
||||
- если `revisionTimeMs` равен, но новый `reencryptedAtMs` больше сохранённого, сообщение применяется;
|
||||
- если обе величины равны, сообщение не применяется;
|
||||
- если новая пара (`revisionTimeMs`, `reencryptedAtMs`) меньше или равна сохранённой, сообщение не применяется.
|
||||
|
||||
Содержимое `body` при этом сравнении не участвует.
|
||||
|
||||
То есть если сервер уже видел ту же пару (`revisionTimeMs`, `reencryptedAtMs`), он считает, что такая версия у него уже есть.
|
||||
|
||||
### 9.2. Повторный tombstone
|
||||
|
||||
Повторный tombstone означает ситуацию, когда сервер повторно получает то же самое событие удаления:
|
||||
|
||||
- того же сообщения;
|
||||
- или той же переписки.
|
||||
|
||||
Это может произойти из-за:
|
||||
|
||||
- повторной межсерверной доставки;
|
||||
- нескольких `access_servers`;
|
||||
- сетевых retry;
|
||||
- дублирующей пересылки с разных маршрутов.
|
||||
|
||||
Правило:
|
||||
|
||||
- повторный tombstone должен быть полностью безопасен;
|
||||
- если соответствующее удаление уже сохранено, сервер ничего не меняет и просто игнорирует повтор.
|
||||
|
||||
### 9.3. Сообщения старше границы удалённой переписки
|
||||
|
||||
Если для пары пользователей уже есть сохранённая граница удаления переписки, и приходит:
|
||||
|
||||
- обычное сообщение;
|
||||
- удаление одного сообщения;
|
||||
- повторное удаление переписки;
|
||||
- любое другое DM-событие;
|
||||
|
||||
у которого `timeMs` меньше этой границы, сервер:
|
||||
|
||||
- ничего не меняет в БД;
|
||||
- не восстанавливает старую историю;
|
||||
- не применяет это событие повторно.
|
||||
|
||||
Если это старое контентное сообщение или входящая копия, сервер дополнительно перерассылает уже известный tombstone удаления переписки на `access_servers` обеих сторон, чтобы отстающие серверы сами подчистили историю.
|
||||
|
||||
Такие сообщения считаются частью уже удалённой истории.
|
||||
|
||||
### 9.4. Приоритет удаления переписки
|
||||
|
||||
Если сначала пришло удаление переписки, а потом удаление одного старого сообщения из этой переписки, сервер должен:
|
||||
|
||||
- проигнорировать это удаление одного сообщения;
|
||||
- не создавать новых изменений поверх уже удалённой истории.
|
||||
|
||||
То же правило действует и для обычных сообщений, и для редактирований старых сообщений.
|
||||
|
||||
## 10. JSON API v1
|
||||
|
||||
### 10.1. `SendMessagePair`
|
||||
|
||||
Назначение:
|
||||
|
||||
- клиент отправляет новый DM;
|
||||
- клиент отправляет редактирование старого DM;
|
||||
- клиент отправляет парную новую ревизию типов `1/2`.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "SendMessagePair",
|
||||
"requestId": "req-123",
|
||||
"payload": {
|
||||
"incomingBlobB64": "...",
|
||||
"outgoingBlobB64": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- клиенту достаточно отправить пару на один любой доступный сервер;
|
||||
- успешный `status=200` означает локальное сохранение;
|
||||
- первая сетевая попытка выполняется до ответа клиенту, параллельно для двух серверов получателя;
|
||||
- сервер после принятия сам отвечает за дальнейшую межсерверную доставку.
|
||||
|
||||
Ответ сохраняет прежние `baseKey`, `incomingKey`, `outgoingKey` и счётчики доставки в клиентские сессии. Единственное новое поле — `deliveryState`: `accepted`, `delivered` или `failed`.
|
||||
|
||||
### 10.2. `ReceiveIncomingMessage`
|
||||
|
||||
Назначение:
|
||||
|
||||
- приём одной входящей копии по схеме server-to-server;
|
||||
- приём входящего редактирования;
|
||||
- приём входящего read-receipt.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "ReceiveIncomingMessage",
|
||||
"requestId": "req-456",
|
||||
"payload": {
|
||||
"incomingBlobB64": "...",
|
||||
"sourceServerLogin": "server-a"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`sourceServerLogin` пока доверяется без отдельной межсерверной подписи. Сервер всё равно проверяет пользовательскую подпись самого `SHiNE_DM`. Для `ReceiveOutcomingMessage` и `SendMessagePair` пустой `sourceServerLogin` означает клиентский вызов, непустой - peer-вызов. Успешный ответ сохраняет старые поля `messageKey`, `baseKey` и счётчики realtime-доставки.
|
||||
|
||||
### 10.3. `DeleteMessage`
|
||||
|
||||
Назначение:
|
||||
|
||||
- удалить одно сообщение у обеих сторон.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "DeleteMessage",
|
||||
"requestId": "req-789",
|
||||
"payload": {
|
||||
"blobB64": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ожидается контейнер типа:
|
||||
|
||||
- `5`, если удаление инициировал отправитель;
|
||||
- `6`, если удаление инициировал получатель.
|
||||
|
||||
UI-следствие для клиента:
|
||||
|
||||
- пользователь может удалить как своё исходящее сообщение, так и входящее;
|
||||
- для удаления входящего UI должен отправлять вариант, где удаление инициировал получатель (`type=6`).
|
||||
|
||||
### 10.4. `DeleteConversation`
|
||||
|
||||
Назначение:
|
||||
|
||||
- удалить всю переписку до времени самого служебного сообщения.
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "DeleteConversation",
|
||||
"requestId": "req-790",
|
||||
"payload": {
|
||||
"blobB64": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ожидается контейнер типа:
|
||||
|
||||
- `7`, если удаление инициировал отправитель;
|
||||
- `8`, если удаление инициировал получатель.
|
||||
|
||||
UI-следствие для клиента:
|
||||
|
||||
- действие `Очистить историю` должно отправлять служебное сообщение удаления переписки;
|
||||
- для обычной клиентской кнопки очистки истории допускается вариант удаления, инициированный текущим получателем (`type=8`);
|
||||
- после применения такого сообщения UI может оставлять в чате видимую служебную точку отсечения истории;
|
||||
- отдельное UI-действие `Удалить чат` может дополнительно спросить, нужно ли вместе с удалением контакта также отправить `DeleteConversation`.
|
||||
|
||||
### 10.5. `GetDirectMessages`
|
||||
|
||||
Назначение:
|
||||
|
||||
- постранично отдавать историю одного личного диалога;
|
||||
- не вываливать весь старый backlog автоматически в момент логина;
|
||||
- отдавать клиенту только полезные chat-сообщения, без технических receipt/tombstone блоков.
|
||||
|
||||
Правила:
|
||||
|
||||
- начиная с 22 июля 2026 года старая история DM больше не должна автоматически пушиться клиенту сразу после входа;
|
||||
- клиент сам запрашивает первую страницу истории у нужного собеседника;
|
||||
- последующие страницы клиент запрашивает по курсору `beforeTimeMs` + `beforeMessageKey`;
|
||||
- realtime-новые сообщения по-прежнему приходят отдельно через `SignedMessageArrived`.
|
||||
|
||||
Содержимое страницы:
|
||||
|
||||
- сервер возвращает только контентные DM типов `1/2`;
|
||||
- read-receipt (`3/4`) и tombstone удаления (`5/6/7/8`) в историю страницы не включаются;
|
||||
- сообщения идут страницами от новых к старым;
|
||||
- лимит страницы считается по самим chat-сообщениям диалога.
|
||||
|
||||
Поле прочтения:
|
||||
|
||||
- для каждого контентного сообщения сервер может вернуть `readAtMs`;
|
||||
- `readAtMs` означает точное время прочтения сообщения, если оно уже известно серверу;
|
||||
- если точное время для старого сообщения неизвестно, но по более новым данным видно, что сообщение уже точно прочитано, UI может показывать его как прочитанное без точного времени;
|
||||
- read-receipt при этом остаётся отдельным DM-событием синхронизации, но в обычную историю страницы не подмешивается.
|
||||
|
||||
Для исходящих элементов сервер также возвращает единственное поле `deliveryState`.
|
||||
|
||||
## 11. Межсерверная доставка
|
||||
|
||||
### 11.1. Клиентская сторона
|
||||
|
||||
Клиенту достаточно отправить сообщение на:
|
||||
|
||||
- любой один доступный сервер.
|
||||
|
||||
### 11.2. Серверная сторона
|
||||
|
||||
После локального принятия пара получает изменяемое состояние `accepted`. Сервер сразу вызывает до двух текущих маршрутов получателя параллельно. Успех хотя бы одного маршрута переводит сообщение в терминальное `delivered`; ждать второй маршрут не требуется.
|
||||
|
||||
После первой попытки полная пара передаётся единственному второму access-серверу отправителя через прежний `ReceiveOutcomingMessage`. Peer самостоятельно пробует актуальные маршруты получателя; delivery-state в запрос не входит.
|
||||
|
||||
Идемпотентность обязательна. ACK означает запись в БД; повтор уже известной ревизии считается успешным ACK.
|
||||
|
||||
### 11.3. Догоняющая синхронизация истории
|
||||
|
||||
Для восстановления пропущенных DM-событий между access-серверами используется pull-операция:
|
||||
|
||||
- `DmSyncBatch`
|
||||
|
||||
Второй сервер запрашивает outbox-события владельца с `synced=false`, сохраняет их и передаёт подтверждённые `syncId` в `ackSyncIds` следующего запроса. Источник ставит `synced=true` только после ACK. Каждый цикл начинается с начала списка; курсор нужен только для страниц текущего цикла.
|
||||
|
||||
Полная пара отправителя передаётся двумя blob. Входящая копия получателя и tombstone передаются одним blob.
|
||||
|
||||
Полная пара отправителя передаётся одним элементом с двумя blob в порядке incoming/outgoing. Входящая копия получателя и tombstone передаются одним blob.
|
||||
|
||||
При применении событий, полученных через `DmSyncBatch`, сервер:
|
||||
|
||||
- проверяет формат `SHiNE_DM`;
|
||||
- проверяет подпись;
|
||||
- применяет существующие правила ревизий, read-receipt и tombstone;
|
||||
- не отправляет realtime/push-уведомления клиентам;
|
||||
- не запускает повторный fan-out, чтобы не создавать циклы.
|
||||
|
||||
Синхронизация настроек и DM выполняется одним периодическим процессом и через
|
||||
один логический последовательный сеанс поверх постоянного WSS-пула. Закрытие
|
||||
этого логического сеанса не закрывает физическое соединение с peer. Выборка DM
|
||||
использует частичный индекс по `synced=false`.
|
||||
|
||||
В текущей реализации межсерверная авторизация DM ещё не включена. Принимающий сервер проверяет, что сам является access-сервером `ownerLogin`, и всегда проверяет пользовательские подписи signed-блоков.
|
||||
|
||||
### 11.4. Ошибки доставки
|
||||
|
||||
Если ни один сервер получателя не подтвердил запись, попытки выполняются сразу, через 30 секунд, 5 минут, 25 минут и 1 час от первоначального принятия.
|
||||
|
||||
Перед попытками через 5 минут, 25 минут и час сервер read-only спрашивает peer отправителя через `GetDmDeliveryStatus(messageKey)`. Если peer уже доставил хотя бы на один сервер, сообщение считается доставленным.
|
||||
|
||||
После неудачной последней попытки устанавливается `failed`; дальнейших автоматических попыток и кнопки ручного повтора нет.
|
||||
|
||||
## 12. Хранение в БД
|
||||
|
||||
Основная таблица остаётся:
|
||||
|
||||
- `signed_messages`
|
||||
|
||||
В ней должны сохраняться:
|
||||
|
||||
- обычные контентные DM;
|
||||
- read-receipt DM;
|
||||
- tombstone одного сообщения;
|
||||
- tombstone удаления переписки.
|
||||
|
||||
Для контентных сообщений в БД дополнительно должно поддерживаться серверное поле `read_at_ms`, если для этого сообщения уже было принято read-receipt событие.
|
||||
|
||||
Сообщение об удалении одного сообщения хранится в БД и не удаляется физически, чтобы:
|
||||
|
||||
- защищать от повторного приёма старых версий;
|
||||
- не терять факт удаления;
|
||||
- корректно синхронизировать событие между серверами.
|
||||
|
||||
Сообщение об удалении переписки тоже хранится в БД, а старые сообщения до его времени из БД удаляются.
|
||||
|
||||
Изменяемая сетевая часть хранится отдельно:
|
||||
|
||||
- `dm_delivery_state` — состояние и расписание доставки исходящей пары;
|
||||
- `dm_sync_outbox` — событие владельца и единственный флаг ACK `synced`;
|
||||
- частичные индексы содержат только due/unsynced строки.
|
||||
|
||||
Legacy-таблица `dm_sync_peer_state` после миграции v12 физически остаётся для безопасной установки ZIP-накладки, но новым DM-кодом не используется.
|
||||
|
||||
## 13. Что обязательно должно измениться в коде относительно v0.5
|
||||
|
||||
- сервер не должен требовать одинаковый `encryptedBody` у `type=1` и `type=2`;
|
||||
- сервер не должен трактовать `encryptedBody` как обычный UTF-8 текст;
|
||||
- сервер не должен валидировать внутреннюю crypto-структуру `body` у контентных DM `type=1/2`;
|
||||
- DM должны реально шифроваться end-to-end;
|
||||
- удаление одного сообщения должно стать терминальным tombstone;
|
||||
- удаление всей переписки должно стать отдельным служебным событием;
|
||||
- межсерверная маршрутизация DM должна идти через `access_servers`;
|
||||
- сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM;
|
||||
- при выборе актуальной версии должен учитываться `reencryptedAtMs`, если `revisionTimeMs` совпадает;
|
||||
- логика должна быть безопасна для одного или двух серверов у каждой стороны.
|
||||
|
||||
## 14. Что в v1 пока не входит
|
||||
|
||||
- вложения в DM;
|
||||
- хранение отдельного `keyId` шифрования в DM;
|
||||
- ротация `clientKey`;
|
||||
- финальная конкретная UI-реализация массовой перешифровки;
|
||||
- межсерверная авторизация DM-синхронизации и доставки.
|
||||
|
||||
## 15. UI списка диалогов и загрузки истории
|
||||
|
||||
Актуальные правила клиентского интерфейса:
|
||||
|
||||
- превью диалога выбирается по самому новому контентному сообщению независимо от направления `in/out`;
|
||||
- если локально уже есть более новое расшифрованное сообщение, чем в серверной сводке `ListContacts`, клиент использует локальный текст и время до следующего согласования с сервером;
|
||||
- локальное сообщение сравнивается с `lastMessageTimeMs`, поэтому входящие и исходящие сообщения участвуют в выборе на равных;
|
||||
- индикатор загрузки истории всегда является компактным элементом фиксированной высоты сверху списка и не растягивается на высоту пустого чата;
|
||||
- клик по аватару или логину собеседника открывает меню перехода в профиль или связи этого пользователя.
|
||||
|
||||
Эти правила относятся только к UI и не изменяют сетевой или байтовый формат DM v1.
|
||||
- байтовый формат SHiNE_DM;
|
||||
- шифрование входящей и исходящей копий;
|
||||
- пользовательские подписи;
|
||||
- deliveryState и retry-воркер;
|
||||
- read-receipts;
|
||||
- синхронизацию пользовательских блокчейнов через server-PDA sync_servers;
|
||||
- общий постоянный WSS ServerConnectionPool.
|
||||
|
||||
@@ -340,7 +340,13 @@ ReadReceiptBody_v1_0
|
||||
|
||||
В версии DM v1 все типы `1..8` используют единый контейнер `SHiNE_DM`.
|
||||
|
||||
Межсерверные операции `ReceiveOutcomingMessage`, `ReceiveIncomingMessage` и `DmSyncBatch` не вводят новый байтовый формат DM. Они передают уже сохранённые raw-контейнеры `SHiNE_DM` в Base64. `ackSyncIds` подтверждает только факт сохранения синхронизированной ревизии; delivery-state между серверами не передаётся. Принимающий сервер заново проверяет подпись и применяет контейнер по его `messageType`.
|
||||
`SendMessagePair` передаёт входящую и исходящую подписанные копии от UI на
|
||||
сервер отправителя. Внутренняя операция `ReceiveIncomingMessage` передаёт
|
||||
только входящую raw-копию в Base64 на единственный access-сервер получателя.
|
||||
Обе операции используют существующий байтовый формат `SHiNE_DM`.
|
||||
Межсерверные `ReceiveOutcomingMessage` и `DmSyncBatch` удалены.
|
||||
Принимающий сервер заново проверяет пользовательскую подпись и применяет
|
||||
контейнер по его `messageType`.
|
||||
|
||||
## 14. Превью в списке диалогов
|
||||
|
||||
|
||||
Reference in New Issue
Block a user