Довести DM v1 до соответствия коду и документации

This commit is contained in:
AidarKC
2026-07-08 17:28:39 +04:00
parent faada9dcdb
commit 98a2631590
19 changed files with 283 additions and 115 deletions
+2 -1
View File
@@ -71,7 +71,8 @@
Важно:
- это server-side existence-check;
- если пользователя нет в локальной БД, он может быть импортирован при авторизации из Solana PDA.
- если пользователя нет в локальной БД, сервер сразу пытается lazy-import из Solana PDA;
- поэтому `GetUser` можно использовать как актуальный способ получить `clientKey` и базовые поля пользователя перед E2EE DM.
### Запрос
+1 -1
View File
@@ -57,7 +57,6 @@
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
| `SendDirectMessage` | `12_Direct_Messages_Push_Calls_API.md` | отправка подписанного DM-пакета |
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
@@ -70,6 +69,7 @@
## Важные замечания
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
- Legacy-операция `SendDirectMessage` больше не зарегистрирована и не должна использоваться для DM v1.
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
@@ -7,6 +7,11 @@
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`
Важно:
- legacy-операция `SendDirectMessage` отключена и не зарегистрирована в публичном API;
- для DM v1 нужно использовать только `SendMessagePair`, `ReceiveOutcomingMessage`, `ReceiveIncomingMessage`, `DeleteMessage`, `DeleteConversation`.
## 1. `UpsertPushToken`
Требует авторизации.
@@ -111,7 +116,7 @@
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
- `400 / BAD_CRYPTO_METHOD` — неподдерживаемый метод шифрования контейнера `EncryptedBody_v1_0`
- `404 / USER_NOT_FOUND` — один из логинов не найден
- `404 / USER_NOT_FOUND` — один из логинов не найден даже после попытки lazy-import из Solana PDA
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
## 4. `ReceiveIncomingMessage`
@@ -223,6 +228,8 @@
## 11. Замечания
- все DM-типы `1..8` используют `SHiNE_DM`
- `GetUser` может lazy-import пользователя из Solana PDA, поэтому именно через него клиент обычно получает `clientKey` адресата для E2EE
- сервер не расшифровывает DM и не использует ciphertext как preview текста
- сервер хранит последнюю применённую ревизию контентного сообщения и отдельные tombstone-события удаления
- сервер хранит последнюю применённую версию контентного сообщения по правилу `revisionTimeMs`, а при равенстве — по `reencryptedAtMs`
- если сервер уже знает tombstone удаления переписки и получает старое сообщение до этой границы, он перерассылает известный `DeleteConversation` на `access_servers` обеих сторон
- HTTP endpoints для DM-файлов сейчас отсутствуют
@@ -11,9 +11,13 @@
5. Удаление исходящего сообщения отправителем (`type=5`) и удаление входящего сообщения получателем (`type=6`).
6. Удаление истории переписки (`type=7/8`) и игнорирование более старых сообщений после tombstone.
7. Сценарий с несколькими `access_servers`, включая случай общего сервера у обеих сторон.
8. Первый DM пользователю, которого ещё нет в локальной `solana_users`, с lazy-import из Solana PDA.
9. Две версии одного сообщения с одинаковым `revisionTimeMs`, но разным `reencryptedAtMs`: должна победить версия с большим `reencryptedAtMs`.
10. После уже известного `DeleteConversation` отправка старого сообщения с другого сервера должна приводить к повторной рассылке tombstone удаления переписки.
11. Попытка вызвать legacy `SendDirectMessage` должна не работать, а новый UI должен использовать только `SendMessagePair`.
- ожидаемый результат:
Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, lazy-import из PDA позволяет отправить первый DM без ручного прогрева БД, версия с большим `reencryptedAtMs` корректно замещает старую при равном `revisionTimeMs`, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
- статус:
pending
+1
View File
@@ -15,3 +15,4 @@
- код DM и оба документа `Протокол_DM_v1.md` + `Формат_DM_v1.md` всегда должны обновляться синхронно;
- если меняется поведение DM в коде, в том же наборе изменений обновляется и эта документация.
- локальная таблица пользователей для DM считается кэшем, а источником истины остаётся Solana PDA; если серверная логика DM меняет правила lazy-import пользователей из PDA, это тоже обязательно фиксируется в документации.
@@ -71,6 +71,19 @@
- сервер не должен расшифровывать DM;
- ciphertext у `type=1` и `type=2` обычно разный и не обязан совпадать побайтно.
### 2.3. Источник истины по пользователю
Для DM-проверки сервер использует:
- локальную `solana_users` как кэш;
- Solana PDA как источник истины по `clientKey` и `access_servers`.
Если нужного пользователя нет локально, сервер обязан попытаться lazy-import из Solana PDA:
- в `GetUser`;
- при серверной верификации DM-подписи;
- перед межсерверной маршрутизацией через `access_servers`.
## 3. Типы сообщений
В версии v1 используются следующие `messageType`:
@@ -135,7 +148,8 @@
- plaintext может остаться тем же;
- ciphertext меняется;
- `reencryptedAtMs` получает ненулевое значение;
- `revisionTimeMs` увеличивается.
- `revisionTimeMs` может остаться прежним, в том числе нулевым, если менялось только шифрование без редактирования текста;
- сервер при выборе актуальной версии обязан учитывать обе метки времени.
Если сообщение уже удалено одним из типов `5/6`, его перешифровывать больше нельзя.
@@ -223,7 +237,7 @@
то сервер:
- не принимает это сообщение;
- в ответ возвращает то же сообщение удаления переписки;
- распространяет уже известный tombstone удаления переписки на серверы доступа обеих сторон;
- ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки.
### 6.5. Будущие новые сообщения
@@ -281,6 +295,8 @@
- `SendMessagePair`
- `ReceiveOutcomingMessage`
- `ReceiveIncomingMessage`
- `DeleteMessage`
- `DeleteConversation`
Их роли в v1:
@@ -290,34 +306,27 @@
### 8.2. Новые методы, которые нужны
Нужны как минимум отдельные операции:
- `DeleteMessage`
- `DeleteConversation`
Названия могут быть другими, но логически это две разные операции:
- удалить одно сообщение у обеих сторон;
- удалить всю переписку до заданной временной границы.
Отдельный legacy-метод `SendDirectMessage` в DM v1 не используется и должен оставаться отключённым, чтобы не было параллельного старого стека доставки.
## 9. Правила валидации и применения
### 9.1. Общее правило по ревизиям
Для одного и того же `messageKey` сервер сравнивает только:
Для одного и того же контентного сообщения сервер сравнивает пару:
- уже сохранённый `revisionTimeMs`;
- `revisionTimeMs` входящего сообщения.
- `revisionTimeMs`;
- `reencryptedAtMs`.
Правило:
- если новый `revisionTimeMs` больше сохранённого, сообщение применяется;
- если новый `revisionTimeMs` равен сохранённому, сообщение не применяется;
- если новый `revisionTimeMs` меньше сохранённого, сообщение не применяется.
- если `revisionTimeMs` равен, но новый `reencryptedAtMs` больше сохранённого, сообщение применяется;
- если обе величины равны, сообщение не применяется;
- если новая пара (`revisionTimeMs`, `reencryptedAtMs`) меньше или равна сохранённой, сообщение не применяется.
Содержимое `body` при этом сравнении не участвует.
То есть если `revisionTimeMs` совпадает, сервер считает, что такая ревизия у него уже есть.
То есть если сервер уже видел ту же пару (`revisionTimeMs`, `reencryptedAtMs`), он считает, что такая версия у него уже есть.
### 9.2. Повторный tombstone
@@ -353,6 +362,8 @@
- не восстанавливает старую историю;
- не применяет это событие повторно.
Если это старое контентное сообщение или входящая копия, сервер дополнительно перерассылает уже известный tombstone удаления переписки на `access_servers` обеих сторон, чтобы отстающие серверы сами подчистили историю.
Такие сообщения считаются частью уже удалённой истории.
### 9.4. Приоритет удаления переписки
@@ -475,6 +486,8 @@ Request:
Если часть серверов совпадает, это допустимо.
Если один и тот же сервер присутствует у обеих сторон, он не должен слать сообщение сам себе повторно, но обязан локально сохранить событие и доставить его в нужные пользовательские сессии.
Идемпотентность обязательна.
### 11.3. Ошибки доставки
@@ -513,6 +526,9 @@ Request:
- удаление одного сообщения должно стать терминальным tombstone;
- удаление всей переписки должно стать отдельным служебным событием;
- межсерверная маршрутизация DM должна идти через `access_servers`;
- сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM;
- при выборе актуальной версии должен учитываться `reencryptedAtMs`, если `revisionTimeMs` совпадает;
- legacy `SendDirectMessage` должен быть отключён;
- логика должна быть безопасна для нескольких серверов у каждой стороны.
## 14. Что в v1 пока не входит
@@ -81,11 +81,12 @@ SHiNE_DM
### `revisionTimeMs`
- `0` для исходной версии сообщения;
- ненулевое значение для новой ревизии сообщения;
- при редактировании или удалении должно быть больше предыдущего значения для этого же `messageKey`.
- `0` для исходной версии контентного сообщения;
- ненулевое значение для новой текстовой ревизии сообщения;
- для типов `5/6` это время tombstone удаления одного сообщения;
- для типов `7/8` всегда `0`.
То есть это `0` или время, когда сообщение было отредактировано либо удалено.
То есть `revisionTimeMs` отвечает именно за изменение смыслового содержимого сообщения, а не за сам факт перешифровки.
### `reencryptedAtMs`
@@ -94,6 +95,13 @@ SHiNE_DM
Для типов `5/6/7/8` должно быть `0`.
`reencryptedAtMs` не требует, чтобы `revisionTimeMs` был ненулевым. Допустим сценарий:
- `revisionTimeMs = 0`
- `reencryptedAtMs > 0`
если исходное сообщение не редактировалось, но было перешифровано позже.
### `bodyLen`
- `> 0` для типов `1/2/3/4`;
@@ -213,7 +221,7 @@ ReadReceiptBody_v1_0
### Перешифровка
- `revisionTimeMs > 0`
- `revisionTimeMs = 0` или `> 0`
- `reencryptedAtMs > 0`
## 8. Контент типов `3/4`