SHA256
Документировать новый DM-протокол и формат (в коде пока не реализовано)
This commit is contained in:
@@ -1,214 +0,0 @@
|
||||
# Личные сообщения (DM)
|
||||
|
||||
## Текущее состояние
|
||||
|
||||
Сейчас в проекте реализованы:
|
||||
|
||||
- новый формат контентных личных сообщений `SHiNE_DM`;
|
||||
- ревизии сообщений через `revisionTimeMs`;
|
||||
- редактирование сообщения через повторную отправку той же логической пары;
|
||||
- удаление сообщения через пустую ревизию;
|
||||
- `upsert` последней версии сообщения на сервере.
|
||||
|
||||
Сейчас в проекте **не реализованы**:
|
||||
|
||||
- вложения в DM;
|
||||
- upload/download файлов для DM;
|
||||
- UI-кнопка прикрепления файла;
|
||||
- серверное хранение файловых связей для DM.
|
||||
|
||||
Черновик будущих вложений вынесен отдельно:
|
||||
|
||||
- `Dev_Docs/Personal_Messages/Черновик_будущих_DM_вложений.md`
|
||||
|
||||
## Общая схема
|
||||
|
||||
Личное сообщение по-прежнему отправляется парой signed-блоков:
|
||||
|
||||
- `type=1` — входящий блок для получателя;
|
||||
- `type=2` — исходящая копия для отправителя.
|
||||
|
||||
Read-receipt пока остаются в legacy-формате:
|
||||
|
||||
- `type=3` — входящее подтверждение прочтения;
|
||||
- `type=4` — исходящая копия подтверждения.
|
||||
|
||||
Ключи сообщения:
|
||||
|
||||
- `baseKey = fromLogin|toLogin|timeMs|nonce`
|
||||
- `messageKey = baseKey|messageType`
|
||||
|
||||
Логический идентификатор письма задаётся парой:
|
||||
|
||||
- `timeMs`
|
||||
- `nonce`
|
||||
|
||||
Эти поля не меняются при редактировании или удалении. Меняется только:
|
||||
|
||||
- `revisionTimeMs`
|
||||
- содержимое `encryptedBody`
|
||||
|
||||
Сервер хранит только последнюю версию записи для каждого `messageKey`.
|
||||
|
||||
## Формат контентного DM: `SHiNE_DM`
|
||||
|
||||
Префикс бинарного блока:
|
||||
|
||||
- `SHiNE_DM`
|
||||
|
||||
Поля идут в big-endian порядке:
|
||||
|
||||
1. `formatVersionMajor` (`u8`) = `1`
|
||||
2. `formatVersionMinor` (`u8`) = `0`
|
||||
3. `toLoginLen` (`u8`) + `toLogin` (ASCII, `1..60`)
|
||||
4. `fromLoginLen` (`u8`) + `fromLogin` (ASCII, `1..60`)
|
||||
5. `timeMs` (`u64`)
|
||||
6. `nonce` (`u32`)
|
||||
7. `messageType` (`u16`) — только `1` или `2`
|
||||
8. `revisionTimeMs` (`u64`)
|
||||
9. `attachmentsCount` (`u8`)
|
||||
10. `encryptedBodyLen` (`u32`)
|
||||
11. `encryptedBody` (`bytes`)
|
||||
12. `signature` (`64 bytes`, Ed25519)
|
||||
|
||||
### Ограничения
|
||||
|
||||
- `attachmentsCount` сейчас всегда должен быть `0`
|
||||
- `encryptedBodyLen` сейчас ограничен сервером до `16384` байт
|
||||
- `revisionTimeMs` не может быть отрицательным
|
||||
|
||||
Если приходит `attachmentsCount != 0`, сервер отклоняет такой DM как:
|
||||
|
||||
- `ATTACHMENTS_DISABLED`
|
||||
|
||||
## Legacy read-receipt: `SHiNE_dm2`
|
||||
|
||||
Подтверждения прочтения `type=3/4` пока используют старый контейнер `SHiNE_dm2`:
|
||||
|
||||
1. `toLoginLen` (`u8`) + `toLogin`
|
||||
2. `fromLoginLen` (`u8`) + `fromLogin`
|
||||
3. `timeMs` (`u64`)
|
||||
4. `nonce` (`u32`)
|
||||
5. `messageType` (`u16`) — `3` или `4`
|
||||
6. `payloadLen` (`u16`)
|
||||
7. `payloadBytes`
|
||||
8. `signature`
|
||||
|
||||
## Редактирование
|
||||
|
||||
Редактирование делается новой отправкой той же логической пары сообщения:
|
||||
|
||||
- `timeMs` и `nonce` остаются теми же;
|
||||
- `messageType` остаётся `1/2`;
|
||||
- `revisionTimeMs` становится больше;
|
||||
- `encryptedBody` содержит новую версию текста.
|
||||
|
||||
Если на сервер приходит более старая ревизия, она игнорируется.
|
||||
|
||||
Если приходит та же ревизия и тот же бинарный блок, сервер тоже её не применяет повторно.
|
||||
|
||||
## Удаление
|
||||
|
||||
Удаление личного сообщения делается как новая ревизия того же сообщения:
|
||||
|
||||
- `timeMs` и `nonce` остаются прежними;
|
||||
- `revisionTimeMs` увеличивается;
|
||||
- `attachmentsCount = 0`;
|
||||
- `encryptedBodyLen = 0`;
|
||||
- `encryptedBody` пустой.
|
||||
|
||||
В UI такое сообщение не показывается.
|
||||
|
||||
На сервере это не отдельный тип сообщения, а просто последняя пустая ревизия того же `messageKey`.
|
||||
|
||||
## Поведение сервера
|
||||
|
||||
Для контентных DM сервер:
|
||||
|
||||
1. принимает пару signed-блоков `type=1/2`;
|
||||
2. валидирует формат, подпись и совпадение ключевых полей пары;
|
||||
3. проверяет, что для обеих сторон пары совпадают:
|
||||
- `fromLogin`
|
||||
- `toLogin`
|
||||
- `timeMs`
|
||||
- `nonce`
|
||||
- `revisionTimeMs`
|
||||
- `encryptedBody`
|
||||
4. делает `upsert` последней версии в `signed_messages_v2`;
|
||||
5. сбрасывает pending-доставку по сессиям для новой ревизии;
|
||||
6. рассылает актуальную версию адресатам через `SignedMessageArrived`.
|
||||
|
||||
История старых ревизий сейчас не хранится отдельно: в таблице остаётся только последняя версия по каждому `messageKey`.
|
||||
|
||||
## Хранение в БД
|
||||
|
||||
Основная таблица:
|
||||
|
||||
- `signed_messages_v2`
|
||||
|
||||
Для контентных DM в ней используются:
|
||||
|
||||
- `message_key`
|
||||
- `base_key`
|
||||
- `target_login`
|
||||
- `from_login`
|
||||
- `to_login`
|
||||
- `time_ms`
|
||||
- `nonce`
|
||||
- `message_type`
|
||||
- `revision_time_ms`
|
||||
- `raw_block`
|
||||
- `created_at_ms`
|
||||
|
||||
Отдельных таблиц файлов для DM сейчас нет.
|
||||
|
||||
## События и доставка
|
||||
|
||||
Запрос на отправку по WebSocket остаётся прежним:
|
||||
|
||||
- `SendMessagePair`
|
||||
- `ReceiveOutcomingMessage` как алиас
|
||||
|
||||
Клиент отправляет:
|
||||
|
||||
- `incomingBlobB64`
|
||||
- `outgoingBlobB64`
|
||||
|
||||
Событие в активные сессии:
|
||||
|
||||
- `SignedMessageArrived`
|
||||
|
||||
Если пришла новая ревизия того же сообщения, `messageKey` остаётся прежним, а внутри `blobB64` будет более новый `revisionTimeMs`.
|
||||
|
||||
Подтверждение доставки в сессию:
|
||||
|
||||
- `AckSessionDelivery`
|
||||
|
||||
WebPush и локальные уведомления сейчас работают так:
|
||||
|
||||
- для активной онлайн-сессии приоритет у доставки по WebSocket через `SignedMessageArrived`;
|
||||
- если целевая сессия не онлайн по WebSocket, сервер может отправить WebPush с `kind=new_message`;
|
||||
- если вкладка/приложение живы, но страница скрыта (`document.visibilityState !== visible`), UI дополнительно пытается показать системное уведомление через `service worker`;
|
||||
- для активной видимой страницы UI проигрывает короткий локальный сигнал на каждое новое входящее DM, если браузер ранее разрешил аудио-контекст после пользовательского жеста;
|
||||
- для скрытой, но живой страницы UI также делает `best effort` сигнал через `vibrate()` и более длинный локальный звук;
|
||||
- эти локальные сигналы не гарантируются браузером: на мобильных устройствах они зависят от политики Chrome/Android/iOS.
|
||||
|
||||
## Правила UI
|
||||
|
||||
UI сейчас работает так:
|
||||
|
||||
- показывает только текст `encryptedBody`;
|
||||
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
|
||||
- не показывает удалённые сообщения;
|
||||
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
|
||||
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
|
||||
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
|
||||
- на видимом экране чата/приложения проигрывает короткий локальный звук на новое входящее DM;
|
||||
- при входящем DM для скрытой, но ещё живой страницы пытается поднять системное уведомление через `service worker`;
|
||||
- не показывает и не принимает вложения.
|
||||
|
||||
## Что обязательно помнить
|
||||
|
||||
- вложения в DM сейчас отключены на уровне протокола и UI;
|
||||
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
|
||||
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
|
||||
Reference in New Issue
Block a user