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