215 lines
9.6 KiB
Markdown
215 lines
9.6 KiB
Markdown
# Личные сообщения (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 больше не актуальны;
|
||
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
|