Документировать новый DM-протокол и формат (в коде пока не реализовано)

This commit is contained in:
AidarKC
2026-07-07 01:19:04 +04:00
parent 31563fe9ed
commit 9c588bd9b5
15 changed files with 1013 additions and 90 deletions
@@ -0,0 +1,526 @@
# Личные сообщения (DM) — спецификация v1
## Статус документа
Этот файл — актуальная логическая спецификация DM-протокола SHiNE следующего этапа.
Важно:
- это целевая спецификация;
- в текущем коде она ещё реализована не полностью;
- при реализации код должен приводиться именно к этой спецификации, а не наоборот.
Документ фиксирует:
- модель DM и смысл типов сообщений;
- правила редактирования, перешифровки и удаления;
- роли существующих API-методов;
- правила межсерверной маршрутизации через `access_servers`;
- общее поведение сервера и БД.
Точный байтовый формат контейнера вынесен отдельно:
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`
Устаревшая предыдущая версия сохранена отдельно:
- `Dev_Docs/Personal_Messages/Спецификация_DM_v0.5_устаревшая.md`
## 1. Основная модель
Личное сообщение в SHiNE хранится как две логические копии одного сообщения:
- `type=1` — входящее сообщение для получателя;
- `type=2` — исходящая копия сообщения для отправителя.
Обе копии имеют общий логический идентификатор:
- `baseKey = fromLogin|toLogin|timeMs|nonce`
Идентификатор конкретной копии:
- `messageKey = baseKey|messageType`
Поля `timeMs` и `nonce` не меняются никогда.
`nonce` обязателен, потому что одного `timeMs` недостаточно для гарантированной уникальности.
## 2. Ключи и шифрование
### 2.1. Общий принцип
У пользователя в PDA публикуется один публичный `clientKey` в формате `Ed25519`.
Из того же ключевого материала стандартным способом выводится ключ `X25519` для E2EE-шифрования DM.
Отдельный публичный `dmEncKey` в текущей архитектуре не хранится.
Подробности стандартного преобразования:
- `Dev_Docs/Протоколы/Преобразование_ED25519_в_X25519.md`
### 2.2. Правило шифрования копий
- `type=1` шифруется на ключ получателя;
- `type=2` шифруется на ключ отправителя.
Следствия:
- входящую копию может прочитать только получатель;
- исходящую копию может прочитать только отправитель;
- сервер не должен расшифровывать DM;
- ciphertext у `type=1` и `type=2` обычно разный и не обязан совпадать побайтно.
## 3. Типы сообщений
В версии v1 используются следующие `messageType`:
- `1` — входящее сообщение;
- `2` — исходящая копия сообщения;
- `3` — входящее подтверждение прочтения;
- `4` — исходящая копия подтверждения прочтения;
- `5` — сообщение удалено отправителем;
- `6` — сообщение удалено получателем;
- `7` — переписка удалена отправителем;
- `8` — переписка удалена получателем.
Типы `1/2` — это обычные контентные DM, в том числе их последующие ревизии.
Типы `3/4` — служебные подтверждения прочтения.
Типы `5/6/7/8` — служебные события удаления без зашифрованного тела.
## 4. Базовые операции
### 4.1. Создание нового сообщения
Новое сообщение отправляется парой блоков через:
- `SendMessagePair`
- `ReceiveOutcomingMessage` как алиас
Пару создаёт автор сообщения.
Пара содержит:
- одну входящую копию `type=1`;
- одну исходящую копию `type=2`;
- одинаковые `fromLogin`, `toLogin`, `timeMs`, `nonce`;
- одинаковый логический plaintext;
- разные ciphertext для разных владельцев копий.
### 4.2. Редактирование сообщения
Редактирование общего текста делает автор сообщения.
При редактировании:
- `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`
то сервер:
- не принимает это сообщение;
- в ответ возвращает то же сообщение удаления переписки;
- ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки.
### 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. Несколько серверов у отправителя и получателя
Протокол должен поддерживать ситуацию, когда:
- у отправителя несколько `access_servers`;
- у получателя несколько `access_servers`;
- часть серверов у сторон совпадает;
- часть серверов уникальна.
Из этого следуют требования:
- все DM-операции должны быть идемпотентны;
- повторное получение уже известного события не должно ломать состояние;
- дубль tombstone должен быть безопасен;
- сервер не должен "оживлять" более старую версию сообщения после уже принятого tombstone.
## 8. Методы и их роли
### 8.1. Существующие методы, которые сохраняются
- `SendMessagePair`
- `ReceiveOutcomingMessage`
- `ReceiveIncomingMessage`
Их роли в v1:
- `SendMessagePair` — новая пара сообщений и редактирование старой пары автором;
- `ReceiveIncomingMessage` — приём одной входящей копии, входящих редактирований и служебных DM-событий;
- `ReceiveOutcomingMessage` — алиас `SendMessagePair`.
### 8.2. Новые методы, которые нужны
Нужны как минимум отдельные операции:
- `DeleteMessage`
- `DeleteConversation`
Названия могут быть другими, но логически это две разные операции:
- удалить одно сообщение у обеих сторон;
- удалить всю переписку до заданной временной границы.
## 9. Правила валидации и применения
### 9.1. Общее правило по ревизиям
Для одного и того же `messageKey` сервер сравнивает только:
- уже сохранённый `revisionTimeMs`;
- `revisionTimeMs` входящего сообщения.
Правило:
- если новый `revisionTimeMs` больше сохранённого, сообщение применяется;
- если новый `revisionTimeMs` равен сохранённому, сообщение не применяется;
- если новый `revisionTimeMs` меньше сохранённого, сообщение не применяется.
Содержимое `body` при этом сравнении не участвует.
То есть если `revisionTimeMs` совпадает, сервер считает, что такая ревизия у него уже есть.
### 9.2. Повторный tombstone
Повторный tombstone означает ситуацию, когда сервер повторно получает то же самое событие удаления:
- того же сообщения;
- или той же переписки.
Это может произойти из-за:
- повторной межсерверной доставки;
- нескольких `access_servers`;
- сетевых retry;
- дублирующей пересылки с разных маршрутов.
Правило:
- повторный tombstone должен быть полностью безопасен;
- если соответствующее удаление уже сохранено, сервер ничего не меняет и просто игнорирует повтор.
### 9.3. Сообщения старше границы удалённой переписки
Если для пары пользователей уже есть сохранённая граница удаления переписки, и приходит:
- обычное сообщение;
- удаление одного сообщения;
- повторное удаление переписки;
- любое другое DM-событие;
у которого `timeMs` меньше этой границы, сервер:
- ничего не меняет в БД;
- не восстанавливает старую историю;
- не применяет это событие повторно.
Такие сообщения считаются частью уже удалённой истории.
### 9.4. Приоритет удаления переписки
Если сначала пришло удаление переписки, а потом удаление одного старого сообщения из этой переписки, сервер должен:
- проигнорировать это удаление одного сообщения;
- не создавать новых изменений поверх уже удалённой истории.
То же правило действует и для обычных сообщений, и для редактирований старых сообщений.
## 10. JSON API v1
### 10.1. `SendMessagePair`
Назначение:
- клиент отправляет новый DM;
- клиент отправляет редактирование старого DM;
- клиент отправляет парную новую ревизию типов `1/2`.
Request:
```json
{
"op": "SendMessagePair",
"requestId": "req-123",
"payload": {
"incomingBlobB64": "...",
"outgoingBlobB64": "..."
}
}
```
Правила:
- клиенту достаточно отправить пару на один любой доступный сервер;
- сервер после принятия сам отвечает за дальнейшую межсерверную доставку.
### 10.2. `ReceiveIncomingMessage`
Назначение:
- приём одной входящей копии по схеме server-to-server;
- приём входящего редактирования;
- приём служебного входящего события удаления;
- приём входящего read-receipt.
Request:
```json
{
"op": "ReceiveIncomingMessage",
"requestId": "req-456",
"payload": {
"incomingBlobB64": "..."
}
}
```
### 10.3. `DeleteMessage`
Назначение:
- удалить одно сообщение у обеих сторон.
Request:
```json
{
"op": "DeleteMessage",
"requestId": "req-789",
"payload": {
"blobB64": "..."
}
}
```
Ожидается контейнер типа:
- `5`, если удаление инициировал отправитель;
- `6`, если удаление инициировал получатель.
### 10.4. `DeleteConversation`
Назначение:
- удалить всю переписку до времени самого служебного сообщения.
Request:
```json
{
"op": "DeleteConversation",
"requestId": "req-790",
"payload": {
"blobB64": "..."
}
}
```
Ожидается контейнер типа:
- `7`, если удаление инициировал отправитель;
- `8`, если удаление инициировал получатель.
## 11. Межсерверная доставка
### 11.1. Клиентская сторона
Клиенту достаточно отправить сообщение на:
- любой один доступный сервер.
### 11.2. Серверная сторона
После принятия валидного события сервер должен отправлять его:
- на все серверы из `access_servers` отправителя;
- на все серверы из `access_servers` получателя.
Если часть серверов совпадает, это допустимо.
Идемпотентность обязательна.
### 11.3. Ошибки доставки
Если часть серверов временно недоступна:
- это не должно отменять локальное принятие уже валидного сообщения;
- повторная доставка может делаться отдельным retry-механизмом;
- повторное получение того же события должно быть безопасным.
## 12. Хранение в БД
Основная таблица остаётся:
- `signed_messages_v2`
В ней должны сохраняться:
- обычные контентные DM;
- tombstone одного сообщения;
- tombstone удаления переписки.
Сообщение об удалении одного сообщения хранится в БД и не удаляется физически, чтобы:
- защищать от повторного приёма старых версий;
- не терять факт удаления;
- корректно синхронизировать событие между серверами.
Сообщение об удалении переписки тоже хранится в БД, а старые сообщения до его времени из БД удаляются.
## 13. Что обязательно должно измениться в коде относительно v0.5
- сервер не должен требовать одинаковый `encryptedBody` у `type=1` и `type=2`;
- сервер не должен трактовать `encryptedBody` как обычный UTF-8 текст;
- DM должны реально шифроваться end-to-end;
- удаление одного сообщения должно стать терминальным tombstone;
- удаление всей переписки должно стать отдельным служебным событием;
- межсерверная маршрутизация DM должна идти через `access_servers`;
- логика должна быть безопасна для нескольких серверов у каждой стороны.
## 14. Что в v1 пока не входит
- вложения в DM;
- хранение отдельного `keyId` шифрования в DM;
- ротация `clientKey`;
- финальная конкретная UI-реализация массовой перешифровки;
- физическая полная реализация DM federation в текущем коде.
@@ -1,4 +1,20 @@
# Личные сообщения (DM)
# Личные сообщения (DM) — v0.5 устаревшая спецификация
## Статус документа
Этот документ устарел и сохранён только как историческое описание ранее реализованной схемы DM.
Aктуальная целевая спецификация:
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`
Что в этом документе считать устаревшим:
- трактовку `encryptedBody` как фактически одинакового содержимого пары;
- отсутствие нормального E2EE-шифрования DM;
- старую модель удаления через пустую ревизию без терминального tombstone;
- старую трактовку обновления только как общей пары без отдельной будущей модели перешифровки;
- все упоминания legacy-формата read-receipt как части целевой архитектуры следующего этапа.
## Текущее состояние
@@ -0,0 +1,298 @@
# Формат DM v1
## Статус документа
Этот файл фиксирует байтовый формат DM-контейнеров SHiNE v1.
Важно:
- это целевой формат следующего этапа;
- в текущем коде он ещё реализован не полностью;
- особенно это касается перехода всех типов `1..8` на единый контейнер `SHiNE_DM`.
Логика протокола, API и поведение сервера описаны отдельно:
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`
## 1. Общие правила
- Числа кодируются в `big-endian`.
- `toLogin` и `fromLogin` — ASCII строки длиной `1..60`.
- Подпись — `Ed25519`, 64 байта.
- `nonce``u32`.
- `messageType``u8`.
- `timeMs`, `revisionTimeMs`, `reencryptedAtMs``u64`.
## 2. Типы сообщений
- `1` — входящее сообщение
- `2` — исходящая копия сообщения
- `3` — входящее подтверждение прочтения
- `4` — исходящая копия подтверждения прочтения
- `5` — сообщение удалено отправителем
- `6` — сообщение удалено получателем
- `7` — переписка удалена отправителем
- `8` — переписка удалена получателем
## 3. Контейнер `SHiNE_DM`
```text
SHiNE_DM
- prefix = "SHiNE_DM"
- formatVersionMajor: u8
- formatVersionMinor: u8
- toLoginLen: u8
- toLogin
- fromLoginLen: u8
- fromLogin
- timeMs: u64
- nonce: u32
- messageType: u8
- revisionTimeMs: u64
- reencryptedAtMs: u64
- bodyLen: u32
- body
- signature: [64]
```
## 4. Смысл полей `SHiNE_DM`
### `timeMs`
Это время исходного создания сообщения.
Для типов `7/8` это время самого служебного сообщения удаления переписки и одновременно граница, раньше которой сообщения этой пары считаются недействительными.
### `nonce`
Дополнительное поле уникальности сообщения.
Используется вместе с `timeMs` для построения стабильного логического ID.
### `messageType`
Определяет смысл контейнера:
- обычное сообщение;
- исходящая копия;
- read-receipt;
- удаление одного сообщения;
- удаление всей переписки.
### `revisionTimeMs`
- `0` для исходной версии сообщения;
- ненулевое значение для новой ревизии сообщения;
- при редактировании или удалении должно быть больше предыдущего значения для этого же `messageKey`.
То есть это `0` или время, когда сообщение было отредактировано либо удалено.
### `reencryptedAtMs`
- `0`, если перешифровки не было;
- ненулевое значение, если копия сообщения была перешифрована.
Для типов `5/6/7/8` должно быть `0`.
### `bodyLen`
- `> 0` для типов `1/2/3/4`;
- `0` для типов `5/6/7/8`;
- для всех типов используется один и тот же контейнер `SHiNE_DM`.
## 5. Поле `body` для типов `1/2`
Для обычного контентного DM поле `body` содержит не голый ciphertext, а контейнер шифрования:
```text
EncryptedBody_v1_0
- cryptoMethod: u8
- cryptoVersion: u8
- ephemeralPubKeyLen: u8
- ephemeralPubKey: bytes[ephemeralPubKeyLen]
- ivLen: u8
- iv: bytes[ivLen]
- cipherTextLen: u32
- cipherText: bytes[cipherTextLen]
```
### Смысл полей `EncryptedBody_v1_0`
#### `cryptoMethod`
Идентификатор метода шифрования.
В версии v1 резервируется:
- `1``X25519 + HKDF-SHA256 + AES-256-GCM`
#### `cryptoVersion`
Версия конкретного метода шифрования.
Для текущей версии:
- `0`
#### `ephemeralPubKey`
Публичный ephemeral `X25519` ключ отправителя конкретной ревизии.
Практически ожидается длина:
- `32`
#### `iv`
Nonce/IV для `AES-GCM`.
Практически ожидается длина:
- `12`
#### `cipherText`
Зашифрованное содержимое сообщения.
Сервер не должен трактовать это поле как UTF-8 текст и не должен пытаться расшифровывать его в обычной DM-логике.
## 6. Поле `body` для типов `3/4`
Для read-receipt типов `3/4` поле `body` содержит открытый контейнер ссылки на исходное сообщение:
```text
ReadReceiptBody_v1_0
- refToLoginLen: u8
- refToLogin
- refFromLoginLen: u8
- refFromLogin
- refTimeMs: u64
- refNonce: u32
```
Смысл:
- `refToLogin`
- `refFromLogin`
- `refTimeMs`
- `refNonce`
однозначно указывают на логическое сообщение по его `baseKey`.
Отдельный `refType` в v1 не нужен, потому что подтверждение прочтения относится к самому логическому сообщению, а не к выбору между копиями `type=1` и `type=2`.
## 7. Контент типов `1/2`
Для типов `1/2`:
- `bodyLen > 0`
- `body` обязан быть контейнером `EncryptedBody_v1_0`
- ciphertext у `type=1` и `type=2` может быть разным
### Новое сообщение
- `revisionTimeMs = 0`
- `reencryptedAtMs = 0`
### Редактирование
- `revisionTimeMs > 0`
- `reencryptedAtMs = 0` или больше нуля, если одновременно произошла перешифровка
### Перешифровка
- `revisionTimeMs > 0`
- `reencryptedAtMs > 0`
## 8. Контент типов `3/4`
Типы:
- `3` — входящее подтверждение прочтения
- `4` — исходящая копия подтверждения прочтения
Правила:
- `bodyLen > 0`
- `body` обязан быть контейнером `ReadReceiptBody_v1_0`
- `revisionTimeMs = 0`
- `reencryptedAtMs = 0`
Тип `3` и тип `4` по стилю полностью подчиняются общему контейнеру `SHiNE_DM`:
- тот же `prefix = "SHiNE_DM"`
- те же `formatVersionMajor = 1`
- те же `formatVersionMinor = 0`
Различается только `messageType` и формат `body`.
## 9. Контент типов `5/6`
Типы:
- `5` — сообщение удалено отправителем
- `6` — сообщение удалено получателем
Правила:
- `bodyLen = 0`
- `body` отсутствует
- `revisionTimeMs > 0`
- `reencryptedAtMs = 0`
Это terminal tombstone конкретного сообщения.
После принятия такого контейнера содержательная версия этого же `messageKey` больше не должна приниматься.
## 10. Контент типов `7/8`
Типы:
- `7` — переписка удалена отправителем
- `8` — переписка удалена получателем
Правила:
- `bodyLen = 0`
- `body` отсутствует
- `revisionTimeMs = 0`
- `reencryptedAtMs = 0`
Здесь роль границы удаления играет:
- `timeMs` самого контейнера
После принятия такого контейнера:
- более старые сообщения этой пары должны быть удалены;
- более старые новые поступления этой пары не должны приниматься.
## 11. Подпись
Поле `signature` всегда подписывает весь контейнер от `prefix` до конца `body` включительно.
Правило подписи по `messageType`:
- `1`, `2`, `5`, `7` подписывает отправитель сообщения;
- `6`, `8` подписывает получатель сообщения.
## 12. Какие поля должны совпадать у пары `1/2`
При обычной парной отправке через `SendMessagePair` у двух копий должны совпадать:
- `fromLogin`
- `toLogin`
- `timeMs`
- `nonce`
- `revisionTimeMs`
Дополнительно:
- логический plaintext должен быть одинаковым;
- ciphertext может различаться;
- `reencryptedAtMs` при одной парной ревизии тоже должен совпадать.
## 13. Примечание о текущем коде
В текущем коде типы `3/4` пока ещё реализованы старым контейнером `SHiNE_dm2`.
Для версии протокола v1 целевой формат фиксируется уже как единый `SHiNE_DM` для всех типов `1..8`.
@@ -1,73 +0,0 @@
# Черновик будущих вложений в DM
## Важно
Этот документ описывает только ранний черновик идеи.
Сейчас в проекте **нет** поддержки вложений в личных сообщениях:
- в реализованном формате `SHiNE_DM` поле `attachmentsCount` пока всегда должно быть `0`;
- UI не показывает кнопку прикрепления файлов;
- сервер не принимает upload файлов для DM;
- сервер не раздаёт специальные DM-файлы по отдельным endpoints;
- сервер не хранит отдельные файловые связи для личных сообщений.
Этот документ нужен только для того, чтобы рядом с актуальной документацией было явно видно:
- какие идеи обсуждались;
- что это **не реализовано**;
- что формат, хранение и способ загрузки потом могут сильно измениться.
## Что обсуждалось
Рассматривался такой общий подход:
- у контентного DM есть внешний список вложений;
- во внешнем формате лежат только технические данные;
- человекочитаемые данные о файле живут внутри зашифрованного тела сообщения;
- один и тот же blob-файл теоретически мог бы переиспользоваться в нескольких сообщениях.
Черновой вариант внешнего списка:
- `attachmentsCount`
- далее для каждого вложения:
- `encFileHashSHA256` (`32 bytes`)
- `encFileSize` (`u64`)
Черновой вариант внутреннего маркера в тексте:
```text
<<file:file-format(1.0):type|fileName|origSize|origHashB64u|encHashB64u|encSize|keyB64u|nonceB64u>>
```
Где обсуждались поля:
- `type`
- `fileName`
- `origSize`
- `origHashB64u`
- `encHashB64u`
- `encSize`
- `keyB64u`
- `nonceB64u`
## Что может измениться
В будущем могут измениться любые части идеи:
- сам бинарный формат;
- способ привязки файлов к сообщению;
- момент загрузки файла относительно отправки сообщения;
- серверное хранение blob-файлов;
- права доступа к скачиванию;
- способ рендера вложения в UI.
Именно поэтому этот файл не надо воспринимать как актуальную спецификацию.
## Источник истины на сейчас
Актуальное состояние личных сообщений описано только в:
- `Dev_Docs/Personal_Messages/README.md`
Если между этим черновиком и основным README есть расхождение, верным считается `README.md`.