SHA256
Убрали старый sync и обновили bundle
Что сделано: вычистили неиспользуемый user-settings sync/DM sync хвост, сохранили сборку, обновили bundle.sh так, чтобы gradle-wrapper.jar всегда попадал в архив. Проверено: compileJava и deploy на t2 (server + UI). Не проверяли: полные интеграционные сценарии, ручные UI-флоу и продовый деплой.
This commit is contained in:
@@ -63,14 +63,9 @@
|
||||
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
||||
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
||||
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
||||
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | старая полная DM-пара для второго access-сервера отправителя |
|
||||
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
||||
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | внутренняя доставка входящего DM-блока на единственный сервер получателя |
|
||||
| `DeleteMessage` | `12_Direct_Messages_Push_Calls_API.md` | tombstone одного личного сообщения у обеих сторон |
|
||||
| `DeleteConversation` | `12_Direct_Messages_Push_Calls_API.md` | tombstone удаления истории переписки |
|
||||
| `DmSyncBatch` | `12_Direct_Messages_Push_Calls_API.md` | pull событий `synced=false` с ACK предыдущей страницы |
|
||||
| `GetDmDeliveryStatus` | `12_Direct_Messages_Push_Calls_API.md` | read-only статус доставки по существующему `messageKey` |
|
||||
| `UserSettingsSyncBatch` | `13_User_Settings_API.md` | межсерверная догоняющая синхронизация пользовательских настроек по курсору |
|
||||
| `MarkAllUserSettingsUnsynced` | `13_User_Settings_API.md` | служебная пометка всех настроек как несинхронизированных |
|
||||
| `GetDirectMessages` | `12_Direct_Messages_Push_Calls_API.md` | постраничная загрузка истории личного диалога |
|
||||
| `AckSessionDelivery` | `12_Direct_Messages_Push_Calls_API.md` | подтверждение доставки в сессию |
|
||||
| `CallInviteBroadcast` | `12_Direct_Messages_Push_Calls_API.md` | broadcast приглашения к звонку |
|
||||
@@ -78,7 +73,9 @@
|
||||
|
||||
## Важные замечания
|
||||
|
||||
- `ReceiveOutcomingMessage` зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`, и сохраняет прежний межсерверный payload.
|
||||
- UI отправляет полную подписанную пару только через `SendMessagePair`.
|
||||
- `ReceiveIncomingMessage` вызывается сервером отправителя для доставки одной
|
||||
входящей копии на единственный access-сервер получателя.
|
||||
- Межсерверные DM-операции пока доверяют `sourceServerLogin`; отдельная межсерверная авторизация запланирована позднее.
|
||||
- `ServerHello` пока принимает заявленный `serverLogin` на доверии и не является криптографической аутентификацией.
|
||||
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
|
||||
Важно:
|
||||
|
||||
- для DM v1 нужно использовать `SendMessagePair`, `ReceiveOutcomingMessage`, `ReceiveIncomingMessage`, `DeleteMessage`, `DeleteConversation`, `GetDirectMessages`;
|
||||
- `DmSyncBatch` предназначен для межсерверной догоняющей синхронизации, не для обычного клиентского UI.
|
||||
- UI использует `SendMessagePair`, `DeleteMessage`, `DeleteConversation`
|
||||
и `GetDirectMessages`;
|
||||
- `ReceiveIncomingMessage` — внутренняя доставка одной входящей копии на
|
||||
единственный access-сервер получателя.
|
||||
- сервер поддерживает материализованный слой диалогов `dm_dialog_state`; `read receipt` обновляет серверный watermark и `unreadCount`, а не только локальный клиентский флаг.
|
||||
- в `dm_dialog_state` сервер также хранит `last_message_blob_b64` для последнего контентного DM в base64, чтобы клиент мог отрисовать список чатов без дополнительного запроса.
|
||||
|
||||
@@ -69,9 +71,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
## 3. `SendMessagePair` и `ReceiveOutcomingMessage`
|
||||
|
||||
`ReceiveOutcomingMessage` — алиас `SendMessagePair`.
|
||||
## 3. `SendMessagePair`
|
||||
|
||||
### Назначение
|
||||
|
||||
@@ -114,7 +114,10 @@
|
||||
}
|
||||
```
|
||||
|
||||
Успешный `status=200` подтверждает локальное сохранение. До ответа клиенту сервер параллельно пробует оба актуальных сервера получателя. Поэтому `deliveryState` уже может быть `delivered`; если никто не ответил, возвращается `accepted`.
|
||||
Успешный `status=200` подтверждает локальное сохранение. До ответа клиенту
|
||||
сервер пробует единственный актуальный сервер получателя. Поэтому
|
||||
`deliveryState` уже может быть `delivered`; если сервер получателя не
|
||||
ответил, возвращается `accepted`.
|
||||
|
||||
Возможные `deliveryState`: `accepted`, `delivered`, `failed`.
|
||||
|
||||
@@ -132,7 +135,8 @@
|
||||
|
||||
### Назначение
|
||||
|
||||
Используется там, где нужно принять только incoming-вариант сообщения.
|
||||
Используется сервером отправителя для доставки incoming-варианта сообщения на
|
||||
единственный access-сервер получателя.
|
||||
|
||||
Принимаемые типы:
|
||||
|
||||
@@ -152,7 +156,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
`sourceServerLogin` необязателен для совместимости. Пока межсерверная авторизация отложена, это поле считается доверенным. Пользовательская подпись signed-блока проверяется всегда. Пустой `sourceServerLogin` трактуется как клиентский вызов, непустой - как peer-вызов.
|
||||
`sourceServerLogin` пока передаётся для диагностики маршрута. Межсерверная
|
||||
авторизация остаётся отдельным будущим этапом, но пользовательская подпись
|
||||
signed-блока проверяется всегда. Официальный UI эту операцию не вызывает.
|
||||
|
||||
Успешный ответ содержит существующие `messageKey`, `baseKey` и счётчики realtime-доставки. Повтор уже сохранённой той же ревизии обрабатывается идемпотентно.
|
||||
|
||||
@@ -262,31 +268,11 @@
|
||||
|
||||
Поле `deliveryState` заполняется для исходящих элементов `type=2`. У входящих `type=1` оно отсутствует/равно `null`.
|
||||
|
||||
## 8. Межсерверные операции DM
|
||||
## 8. Внутренняя межсерверная доставка DM
|
||||
|
||||
До отдельного этапа server-auth поля `sourceServerLogin` доверяются. Каждый `SHiNE_DM` всё равно заново проходит проверку формата и пользовательской подписи.
|
||||
### 8.1. `ReceiveIncomingMessage`
|
||||
|
||||
### 8.1. `ReceiveOutcomingMessage`
|
||||
|
||||
Прежняя операция передачи полной пары второму access-серверу отправителя. Delivery-state в межсерверный запрос не входит.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "ReceiveOutcomingMessage",
|
||||
"requestId": "dm-peer-001",
|
||||
"payload": {
|
||||
"incomingBlobB64": "BASE64_INCOMING",
|
||||
"outgoingBlobB64": "BASE64_OUTGOING",
|
||||
"sourceServerLogin": "server-a"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ имеет прежний формат `SendMessagePair`. Принимающий сервер сохраняет пару идемпотентно и самостоятельно ставит локальную задачу доставки.
|
||||
|
||||
### 8.2. `ReceiveIncomingMessage`
|
||||
|
||||
Прежняя операция передачи одной входящей копии серверу получателя или второму серверу самого получателя.
|
||||
Передаёт одну входящую копию на единственный access-сервер получателя.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -301,72 +287,9 @@
|
||||
|
||||
Успешный 2xx-ответ означает, что signed-блок проверен и находится в БД (новая или идемпотентно повторённая запись).
|
||||
|
||||
### 8.3. `DmSyncBatch`
|
||||
|
||||
Pull-синхронизация событий владельца с `synced=false`. `ackSyncIds` подтверждает версии событий, успешно сохранённые из предыдущего ответа.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "DmSyncBatch",
|
||||
"requestId": "dm-sync-001",
|
||||
"payload": {
|
||||
"ownerLogin": "alice",
|
||||
"afterStoredAtMs": 0,
|
||||
"afterMessageKey": "",
|
||||
"limit": 500,
|
||||
"maxBytes": 3000000,
|
||||
"ackSyncIds": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ возвращает страницу несинхронизированных событий и курсор внутри текущего цикла:
|
||||
|
||||
```json
|
||||
{
|
||||
"nextStoredAtMs": 1774700001123,
|
||||
"nextMessageKey": "alice|bob|1774700000123|123456789|2",
|
||||
"hasMore": false,
|
||||
"items": [
|
||||
{
|
||||
"syncId": "alice|bob|1774700000123|123456789|2:0:0",
|
||||
"primaryMessageKey": "alice|bob|1774700000123|123456789|2",
|
||||
"storedAtMs": 1774700001123,
|
||||
"blobsB64": ["BASE64_INCOMING", "BASE64_OUTGOING"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Для пары порядок всегда incoming/outgoing; для входящей копии и tombstone массив содержит один blob. `syncId` — технический идентификатор конкретной ревизии только для ACK синхронизации, а не второй ID сообщения. После сохранения вызывающий сервер передаёт `syncId` в `ackSyncIds` следующего запроса. Только тогда источник ставит `synced=true`. Новый цикл начинается с `afterStoredAtMs=0`; полный сброс флагов поэтому повторно отдаёт всю историю.
|
||||
|
||||
### 8.4. `GetDmDeliveryStatus`
|
||||
|
||||
Единственная новая межсерверная операция доставки. Read-only проверка существующего `messageKey`; не изменяет состояние отвечающего сервера.
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetDmDeliveryStatus",
|
||||
"requestId": "dm-status-001",
|
||||
"payload": {
|
||||
"messageKey": "alice|bob|1774700000123|123456789|2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ содержит `messageKey`, `known` и `delivered`. `delivered=true` означает доставку хотя бы одному серверу получателя.
|
||||
|
||||
Периодический процесс использует один логический последовательный сеанс поверх
|
||||
постоянного WSS-пула: сначала синхронизирует настройки, затем вызывает
|
||||
`DmSyncBatch` до завершения страниц и ACK. Существующий
|
||||
`MarkAllUserSettingsUnsynced` также сбрасывает DM-флаги и возвращает
|
||||
`dmUpdated`; отдельной операции `MarkAllDmUnsynced` нет.
|
||||
|
||||
Основные ошибки межсерверных операций:
|
||||
|
||||
- `400 / EMPTY_OWNER_LOGIN`, `EMPTY_MESSAGE_KEY`;
|
||||
- `403 / LOCAL_SERVER_NOT_ACCESS_SERVER`;
|
||||
- `500 / LOCAL_SERVER_NOT_CONFIGURED`.
|
||||
Операции `ReceiveOutcomingMessage`, `DmSyncBatch` и
|
||||
`GetDmDeliveryStatus` удалены вместе с репликацией между access-серверами
|
||||
одного пользователя.
|
||||
|
||||
## 9. `AckSessionDelivery`
|
||||
|
||||
|
||||
@@ -1,104 +1,57 @@
|
||||
# API пользовательских настроек
|
||||
# User Settings API
|
||||
|
||||
Этот раздел описывает `user_settings` - отдельное хранилище пользовательских настроек, не связанное с `users_params` и не связанное с legacy DM-таблицами.
|
||||
user_settings хранит технические настройки пользователя только на его
|
||||
единственном действующем access-сервере. Межсерверной синхронизации настроек
|
||||
нет. При смене access-сервера старые настройки автоматически не переносятся.
|
||||
|
||||
## 1. Назначение
|
||||
## Подпись
|
||||
|
||||
`user_settings` хранит технические пользовательские настройки, которые должны синхронизироваться между максимум двумя access/sync-серверами пользователя.
|
||||
UI формирует строку:
|
||||
|
||||
Основной текущий кейс:
|
||||
SHiNe/UserSettings:<login>|<setting_type>|<setting_key>|<time_ms>|<value_text>|<value_num>
|
||||
|
||||
- `setting_type = 1` - курсор прочитанности канала;
|
||||
- `setting_key = ownerBlockchainName/channelName`;
|
||||
- `value_num = number of messages already seen in channel`;
|
||||
- `value_text = ''`.
|
||||
и подписывает её текущим Ed25519 clientKey пользователя. Сервер:
|
||||
|
||||
Если настройки нет, канал считается полностью прочитанным, то есть unread = `0`.
|
||||
После первого открытия канала UI отправляет текущий курсор, чтобы зафиксировать baseline и дальше считать только новые сообщения.
|
||||
1. находит пользователя;
|
||||
2. проверяет совпадение client_key с актуальным ключом пользователя;
|
||||
3. строго проверяет подпись;
|
||||
4. при неверной подписи возвращает INVALID_SIGNATURE;
|
||||
5. сохраняет запись только если time_ms новее локальной версии.
|
||||
|
||||
## 2. Структура записи
|
||||
## Операции
|
||||
|
||||
- `login` - логин владельца настройки;
|
||||
- `setting_type` - числовой код типа настройки;
|
||||
- `setting_key` - строковый ключ настройки;
|
||||
- `time_ms` - время установки значения в миллисекундах;
|
||||
- `value_text` - строковое значение;
|
||||
- `value_num` - числовое значение;
|
||||
- `client_key` - публичный Ed25519 ключ клиента в Base64;
|
||||
- `signature` - Ed25519 подпись preimage в Base64;
|
||||
- `synced` - была ли настройка успешно доставлена на второй сервер.
|
||||
### UpsertUserSetting
|
||||
|
||||
Уникальность: `(login, setting_type, setting_key)`.
|
||||
Обновление: только если `time_ms` новее текущего значения.
|
||||
Обязательные поля:
|
||||
|
||||
## 3. Формат подписи
|
||||
- login;
|
||||
- setting_type;
|
||||
- setting_key;
|
||||
- time_ms;
|
||||
- client_key;
|
||||
- signature.
|
||||
|
||||
Подписывается строка:
|
||||
Значения:
|
||||
|
||||
`SHiNe/UserSettings:|login|setting_type|setting_key|time_ms|value_text|value_num`
|
||||
- value_text — строковое значение, по умолчанию пустая строка;
|
||||
- value_num — числовое значение, по умолчанию 0.
|
||||
|
||||
Где внутри полей используется экранирование `\` и `|`.
|
||||
Поля sync_delivery и synced удалены.
|
||||
|
||||
Подпись создаётся клиентским `client_key`.
|
||||
### GetUserSetting
|
||||
|
||||
## 4. Операции
|
||||
Возвращает одну локальную настройку по login, setting_type и setting_key.
|
||||
|
||||
### `UpsertUserSetting`
|
||||
### ListUserSettings
|
||||
|
||||
Записывает или обновляет настройку пользователя.
|
||||
Возвращает все локальные настройки пользователя.
|
||||
|
||||
Если запрос пришёл от клиента, сервер:
|
||||
## Удалённые операции
|
||||
|
||||
- сохраняет запись локально;
|
||||
- пытается сразу отправить её на доступный sync-сервер;
|
||||
- если отправка успешна, помечает запись как `synced=true`;
|
||||
- если нет, оставляет `synced=false`.
|
||||
После перехода на один access-сервер удалены:
|
||||
|
||||
Если запрос пришёл по синхронизации между серверами, используется `sync_delivery=true`, и повторной пересылки дальше не делается.
|
||||
- UserSettingsSyncBatch;
|
||||
- MarkAllUserSettingsUnsynced.
|
||||
|
||||
### `GetUserSetting`
|
||||
|
||||
Чтение одной настройки по `(login, setting_type, setting_key)`.
|
||||
|
||||
### `ListUserSettings`
|
||||
|
||||
Список всех настроек пользователя.
|
||||
|
||||
### `UserSettingsSyncBatch`
|
||||
|
||||
Внутренний межсерверный batch-эндпоинт.
|
||||
|
||||
- отдаёт настройки, новые относительно курсора;
|
||||
- используется для bootstrap и догрузки после восстановления;
|
||||
- применяется только для пользователей, чей сервер есть в `access_servers`.
|
||||
|
||||
### `MarkAllUserSettingsUnsynced`
|
||||
|
||||
Внутренний служебный запрос.
|
||||
|
||||
- помечает все настройки пользователя или все настройки сразу как `synced=false`;
|
||||
- одновременно сбрасывает DM-outbox того же пользователя (или всех пользователей), чтобы операция замены/добавления access-сервера не оставила историю DM несинхронизированной;
|
||||
- ответ дополнительно содержит `dmUpdated` — количество сброшенных DM-событий;
|
||||
- нужен после добавления нового sync-сервера или при потере локальной БД.
|
||||
|
||||
## 5. Синхронизация
|
||||
|
||||
Настройки и DM имеют раздельные таблицы и правила ACK, но периодический процесс
|
||||
использует один логический последовательный сеанс поверх постоянного WSS-пула:
|
||||
сначала синхронизирует настройки, затем забирает `DmSyncBatch`. Завершение
|
||||
логического сеанса не закрывает физический сокет.
|
||||
|
||||
- локальная запись создаётся с `synced=false`, если её ещё не подтвердил второй сервер;
|
||||
- если запись пришла с другого сервера, она сохраняется сразу как `synced=true`;
|
||||
- периодический sync раз в 6 часов проверяет несинхронизированные настройки, догружает их по курсору и затем забирает несинхронизированные DM;
|
||||
- если появляется новый sync-сервер или локальная БД была потеряна, нужно пометить все настройки несинхронизированными и заново догрузить batch с нуля.
|
||||
|
||||
## 6. Текущий UI-кейс
|
||||
|
||||
UI при открытии канала отправляет `UpsertUserSetting` с:
|
||||
|
||||
- `setting_type = 1`;
|
||||
- `setting_key = ownerBlockchainName/channelName`;
|
||||
- `value_num = количество уже просмотренных сообщений в канале`.
|
||||
|
||||
Это значение используется сервером для расчёта unread в списке каналов и в канале.
|
||||
Также удалены таблица peer-cursor и периодический сервис синхронизации
|
||||
настроек.
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
- физическое соединение создаётся одно на `serverLogin`;
|
||||
- логические операции DM, settings и blockchain используют один WSS;
|
||||
- завершение `RemoteSyncSession` не закрывает физический сокет;
|
||||
- известные peer берутся из `sync_servers` и `user_access_servers_current`;
|
||||
- известные peer берутся из `sync_servers` и первых действующих маршрутов
|
||||
`user_access_servers_current`;
|
||||
- список перечитывается каждые 30 секунд;
|
||||
- при изменении URL соединение пересоздаётся;
|
||||
- новые запросы не повторяются транспортом автоматически: действующие
|
||||
@@ -65,8 +66,8 @@
|
||||
|
||||
| Приоритет | Операции |
|
||||
| --- | --- |
|
||||
| `REALTIME` | доставка DM, deletes, `GetDmDeliveryStatus` |
|
||||
| `NORMAL` | настройки, `DmSyncBatch` и access-data sync |
|
||||
| `REALTIME` | доставка DM и signed deletes |
|
||||
| `NORMAL` | прочие межсерверные запросы |
|
||||
| `BULK` | blockchain heads, blocks, `AddBlock` backfill |
|
||||
|
||||
На одном peer одновременно исполняется один запрос. Приоритет применяется к
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
## Смежная документация
|
||||
- [../ИТХ/README.md](../ИТХ/README.md) — ежедневное закрытие блокчейна (ИТХ): краткий обзор.
|
||||
- [../ИТХ/Спецификация_ИТХ_v1.md](../ИТХ/Спецификация_ИТХ_v1.md) — точная спецификация чекпоинтов (Arweave/Solana/канал закрытий).
|
||||
- [sync-between-servers.md](./sync-between-servers.md) — живая межсерверная синхронизация блоков и DM.
|
||||
- [sync-between-servers.md](./sync-between-servers.md) — живая межсерверная синхронизация блокчейна и доставка DM на единственный сервер получателя.
|
||||
|
||||
## Важные ограничения MVP
|
||||
- Каналы `type=100` и `type=200` присутствуют в формате, но сейчас не используются в UI.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Синхронизация блоков и DM между серверами SHiNE
|
||||
# Синхронизация блокчейнов и доставка DM между серверами SHiNE
|
||||
|
||||
Документ описывает архитектуру и протокол синхронизации данных между партнёрскими серверами SHiNE.
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
Каждый сервер регистрирует в своей Solana PDA список `sync_servers` —
|
||||
логины SHiNE-аккаунтов партнёрских серверов, с которыми он синхронизируется.
|
||||
|
||||
Важно: в текущей архитектуре у пользователя одновременно может быть не более
|
||||
двух sync/access-серверов. Это ограничение считается обязательным для runtime-логики
|
||||
`synced` и пользовательских курсоров.
|
||||
`sync_servers` относится к серверному узлу и не является списком
|
||||
access-серверов обычного пользователя. У пользователя действует только первый
|
||||
`access_servers[0]`.
|
||||
|
||||
- Список хранится в блоке `ServerProfileBlock` внутри `user_pda` сервера.
|
||||
- Адрес каждого партнёрского сервера читается из его PDA на Solana.
|
||||
@@ -35,7 +35,7 @@
|
||||
- Сервер-отправитель: сохраняет пару и ставит асинхронную delivery-задачу.
|
||||
- Сервер-получатель: сохраняет входящий блок в `signed_messages`, затем доставляет его активным сессиям.
|
||||
- Дедупликация по уникальному `message_key = from|to|timeMs|nonce|type`.
|
||||
- Репликация между двумя access-серверами пользователя работает через `dm_sync_outbox.synced`, без постоянного time-cursor.
|
||||
- Между access-серверами одного пользователя DM не реплицируются.
|
||||
- Полная актуальная схема: `docs/Personal_Messages/Доставка_и_синхронизация_DM.md`.
|
||||
|
||||
### 3.2 Блоки пользовательского блокчейна
|
||||
@@ -47,10 +47,8 @@
|
||||
|
||||
### 3.3 Пользовательские настройки
|
||||
|
||||
- Отдельная таблица `user_settings`.
|
||||
- Синхронизируются технические настройки пользователя, включая курсор прочитанности каналов.
|
||||
- Для текущего UI-кейса хранится `setting_type = 1` и `setting_key = ownerBlockchainName/channelName`.
|
||||
- Синхронизация идёт с учётом `time_ms` и флага `synced`.
|
||||
Пользовательские настройки хранятся локально на единственном access-сервере и
|
||||
между серверами не синхронизируются.
|
||||
|
||||
## 4. Текущая реализованная схема
|
||||
|
||||
@@ -196,7 +194,7 @@ Full resync запускается только тогда, когда:
|
||||
- На текущем этапе `serverLogin` принимается на доверии; подпись Ed25519 корневым ключом сервера отложена.
|
||||
- При разрыве выполняется переподключение с jitter/backoff до 60 секунд.
|
||||
- После 120 секунд отсутствия полезного трафика отправляется WebSocket ping; pong ожидается 15 секунд.
|
||||
- Один физический канал переиспользуют DM, настройки и blockchain.
|
||||
- Один физический канал переиспользуют доставка DM и blockchain.
|
||||
|
||||
### 5.2 Доставка новых данных (push)
|
||||
|
||||
@@ -208,7 +206,7 @@ Full resync запускается только тогда, когда:
|
||||
|
||||
- При первом подключении к партнёру серверы могут обмениваться курсорами состояния блокчейнов.
|
||||
- Сервер с более полной историей досылает недостающее партнёру.
|
||||
- DM time-cursor удалён: новый peer получает историю после сброса `dm_sync_outbox.synced=false`.
|
||||
- DM-history backfill между access-серверами отсутствует.
|
||||
|
||||
### 5.4 Разрешение конфликтов
|
||||
|
||||
@@ -222,11 +220,11 @@ Full resync запускается только тогда, когда:
|
||||
|
||||
1. Клиент A отправляет пару блоков на свой сервер X.
|
||||
2. Сервер X валидирует и локально сохраняет пару.
|
||||
3. До ответа клиенту X параллельно отправляет входящую копию максимум двум актуальным `access_servers` B.
|
||||
4. ACK хотя бы одного маршрута B означает терминальный `delivered`; второй сервер B догоняется собственной DM-синхронизацией.
|
||||
5. После первой попытки X передаёт полную пару второму access-серверу A старым `ReceiveOutcomingMessage`, без delivery-state.
|
||||
6. При нулевой доставке выполняются повторы через 30 секунд, 5 минут, 25 минут и 1 час. Перед тремя последними X спрашивает peer A через `GetDmDeliveryStatus(messageKey)`.
|
||||
7. После неудачной попытки через час ставится терминальный `failed`.
|
||||
3. До ответа клиенту X отправляет входящую копию на единственный
|
||||
`access_servers[0]` пользователя B через `ReceiveIncomingMessage`.
|
||||
4. Успешное сохранение на сервере B означает терминальный `delivered`.
|
||||
5. При неудаче выполняются повторы через 30 секунд, 5 минут, 25 минут и 1 час.
|
||||
6. После неудачной попытки через час ставится терминальный `failed`.
|
||||
|
||||
Изменения routing B учитываются до терминального состояния, потому что список маршрутов перечитывается на каждой попытке.
|
||||
|
||||
@@ -251,15 +249,14 @@ Full resync запускается только тогда, когда:
|
||||
| Обход Solana RPC через `sync.importUserProfileFromPartner.enabled` | ✅ Реализовано |
|
||||
| Обычный `AddBlock` через `tmp_bch`/`write_check`/`write_pending` | ✅ Реализовано |
|
||||
| Межсерверный постоянный WebSocket-канал | ✅ Реализован общий `ServerConnectionPool` |
|
||||
| Асинхронная доставка DM на access-серверы получателя | ✅ Реализовано |
|
||||
| Асинхронная доставка DM на единственный access-сервер получателя | ✅ Реализовано |
|
||||
| Retry DM до 1 часа + UI-state | ✅ Реализовано |
|
||||
| Репликация DM на второй access-сервер по `synced` | ✅ Реализовано |
|
||||
| Read-only `GetDmDeliveryStatus` | ✅ Реализовано |
|
||||
| Репликация DM и настроек между access-серверами | Не используется |
|
||||
| Push блоков блокчейна партнёрам | ✅ Выполняется через постоянный WSS-пул |
|
||||
| Periodic backfill отсутствующего хвоста | ✅ Реализовано |
|
||||
| Разрешение рассинхрона / divergence | ✅ Реализована базовая full-resync схема во время periodic sync |
|
||||
| Startup recovery по `*.resync_pending` marker-file | ✅ Реализовано |
|
||||
| Маршрутизация DM через один/два `access_servers` | ✅ Реализовано |
|
||||
| Маршрутизация DM только через `access_servers[0]` | ✅ Реализовано |
|
||||
| Криптографическая server-to-server авторизация DM | Нужна реализация |
|
||||
|
||||
Текущая версия сервера использует постоянный WSS-пул для существующих
|
||||
|
||||
@@ -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. Превью в списке диалогов
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
- сервер запускает sync-модуль до продолжения собственного startup;
|
||||
- server startup ждёт входа модуля в состояние `READY`;
|
||||
- актуальный источник истины по пользователям для нового PostgreSQL runtime-слоя: `solana_user_pda_current`.
|
||||
- для быстрого локального маршрутизационного lookup серверов доступа поддерживается вторичная проекция
|
||||
`user_access_servers_current`, которая автоматически пересобирается из `solana_user_pda_current`.
|
||||
- для быстрого локального lookup единственного сервера доступа поддерживается
|
||||
вторичная проекция `user_access_servers_current`, которая автоматически
|
||||
пересобирается только из первого элемента `access_servers[0]` в
|
||||
`solana_user_pda_current`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -291,8 +291,13 @@ AccessServersBlock
|
||||
|
||||
- блок может отсутствовать, если серверы доступа не заданы;
|
||||
- список может обновляться при изменении маршрутизации пользователя;
|
||||
- `access_servers` - логины пользователей системы, используемых как серверы доступа/relay для конкретного пользователя. Через этот список клиентская и серверная логика SHiNE может маршрутизировать доставку личных сообщений, доступ к сессиям и другие пользовательские операции. Solana-программа не обязана проверять, что эти логины действительно зарегистрированы как серверы;
|
||||
- точная семантика выбора сервера доступа определяется клиентской/серверной логикой SHiNE.
|
||||
- `access_servers` — массив логинов серверов доступа/relay. Формат массива
|
||||
сохранён для совместимости, но текущая клиентская и серверная логика использует
|
||||
только первый элемент `access_servers[0]`;
|
||||
- остальные элементы игнорируются без fallback;
|
||||
- официальный UI записывает ровно один сервер и позволяет только заменить его;
|
||||
- Solana-программа не обязана проверять, что логин действительно зарегистрирован
|
||||
как сервер.
|
||||
|
||||
## 14. SessionsBlock
|
||||
|
||||
|
||||
@@ -74,7 +74,8 @@
|
||||
- `server_key: Pubkey`
|
||||
- `server_address: String`
|
||||
- `sync_servers: Vec<String>`
|
||||
- `access_servers: Vec<String>`
|
||||
- `access_servers: Vec<String>` — формат остаётся массивом, но runtime
|
||||
использует только первый элемент
|
||||
- `trusted_count: u8`
|
||||
|
||||
## Главные PDA
|
||||
|
||||
Reference in New Issue
Block a user