# Личные сообщения (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`

## Правила UI

UI сейчас работает так:

- показывает только текст `encryptedBody`;
- умеет обновлять уже существующее сообщение по тому же `messageKey`;
- не показывает удалённые сообщения;
- позволяет владельцу сообщения вызвать меню `Скопировать как текст / Прочесть / Изменить / Удалить`;
- при редактировании показывает над полем ввода полоску `Редактируем сообщение: ...` с кнопкой отмены;
- после редактирования показывает под временем отдельную строку `изменено: <дата время>`;
- не показывает и не принимает вложения.

## Что обязательно помнить

- вложения в DM сейчас отключены на уровне протокола и UI;
- любые старые описания `/f/...`, `/upload` и файловых таблиц для DM больше не актуальны;
- если позже вложения вернутся, их формат и серверная логика могут быть другими.
