SHA256
Довести DM v1 до соответствия коду и документации
This commit is contained in:
@@ -71,7 +71,8 @@
|
|||||||
Важно:
|
Важно:
|
||||||
|
|
||||||
- это server-side existence-check;
|
- это server-side existence-check;
|
||||||
- если пользователя нет в локальной БД, он может быть импортирован при авторизации из Solana PDA.
|
- если пользователя нет в локальной БД, сервер сразу пытается lazy-import из Solana PDA;
|
||||||
|
- поэтому `GetUser` можно использовать как актуальный способ получить `clientKey` и базовые поля пользователя перед E2EE DM.
|
||||||
|
|
||||||
### Запрос
|
### Запрос
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@
|
|||||||
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
|
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
|
||||||
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
||||||
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
||||||
| `SendDirectMessage` | `12_Direct_Messages_Push_Calls_API.md` | отправка подписанного DM-пакета |
|
|
||||||
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
||||||
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
|
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
|
||||||
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
||||||
@@ -70,6 +69,7 @@
|
|||||||
## Важные замечания
|
## Важные замечания
|
||||||
|
|
||||||
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
|
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
|
||||||
|
- Legacy-операция `SendDirectMessage` больше не зарегистрирована и не должна использоваться для DM v1.
|
||||||
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||||
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
||||||
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`
|
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`
|
||||||
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`
|
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- legacy-операция `SendDirectMessage` отключена и не зарегистрирована в публичном API;
|
||||||
|
- для DM v1 нужно использовать только `SendMessagePair`, `ReceiveOutcomingMessage`, `ReceiveIncomingMessage`, `DeleteMessage`, `DeleteConversation`.
|
||||||
|
|
||||||
## 1. `UpsertPushToken`
|
## 1. `UpsertPushToken`
|
||||||
|
|
||||||
Требует авторизации.
|
Требует авторизации.
|
||||||
@@ -111,7 +116,7 @@
|
|||||||
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
|
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
|
||||||
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
|
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
|
||||||
- `400 / BAD_CRYPTO_METHOD` — неподдерживаемый метод шифрования контейнера `EncryptedBody_v1_0`
|
- `400 / BAD_CRYPTO_METHOD` — неподдерживаемый метод шифрования контейнера `EncryptedBody_v1_0`
|
||||||
- `404 / USER_NOT_FOUND` — один из логинов не найден
|
- `404 / USER_NOT_FOUND` — один из логинов не найден даже после попытки lazy-import из Solana PDA
|
||||||
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
|
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
|
||||||
|
|
||||||
## 4. `ReceiveIncomingMessage`
|
## 4. `ReceiveIncomingMessage`
|
||||||
@@ -223,6 +228,8 @@
|
|||||||
## 11. Замечания
|
## 11. Замечания
|
||||||
|
|
||||||
- все DM-типы `1..8` используют `SHiNE_DM`
|
- все DM-типы `1..8` используют `SHiNE_DM`
|
||||||
|
- `GetUser` может lazy-import пользователя из Solana PDA, поэтому именно через него клиент обычно получает `clientKey` адресата для E2EE
|
||||||
- сервер не расшифровывает DM и не использует ciphertext как preview текста
|
- сервер не расшифровывает DM и не использует ciphertext как preview текста
|
||||||
- сервер хранит последнюю применённую ревизию контентного сообщения и отдельные tombstone-события удаления
|
- сервер хранит последнюю применённую версию контентного сообщения по правилу `revisionTimeMs`, а при равенстве — по `reencryptedAtMs`
|
||||||
|
- если сервер уже знает tombstone удаления переписки и получает старое сообщение до этой границы, он перерассылает известный `DeleteConversation` на `access_servers` обеих сторон
|
||||||
- HTTP endpoints для DM-файлов сейчас отсутствуют
|
- HTTP endpoints для DM-файлов сейчас отсутствуют
|
||||||
|
|||||||
@@ -11,9 +11,13 @@
|
|||||||
5. Удаление исходящего сообщения отправителем (`type=5`) и удаление входящего сообщения получателем (`type=6`).
|
5. Удаление исходящего сообщения отправителем (`type=5`) и удаление входящего сообщения получателем (`type=6`).
|
||||||
6. Удаление истории переписки (`type=7/8`) и игнорирование более старых сообщений после tombstone.
|
6. Удаление истории переписки (`type=7/8`) и игнорирование более старых сообщений после tombstone.
|
||||||
7. Сценарий с несколькими `access_servers`, включая случай общего сервера у обеих сторон.
|
7. Сценарий с несколькими `access_servers`, включая случай общего сервера у обеих сторон.
|
||||||
|
8. Первый DM пользователю, которого ещё нет в локальной `solana_users`, с lazy-import из Solana PDA.
|
||||||
|
9. Две версии одного сообщения с одинаковым `revisionTimeMs`, но разным `reencryptedAtMs`: должна победить версия с большим `reencryptedAtMs`.
|
||||||
|
10. После уже известного `DeleteConversation` отправка старого сообщения с другого сервера должна приводить к повторной рассылке tombstone удаления переписки.
|
||||||
|
11. Попытка вызвать legacy `SendDirectMessage` должна не работать, а новый UI должен использовать только `SendMessagePair`.
|
||||||
|
|
||||||
- ожидаемый результат:
|
- ожидаемый результат:
|
||||||
Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
|
Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, lazy-import из PDA позволяет отправить первый DM без ручного прогрева БД, версия с большим `reencryptedAtMs` корректно замещает старую при равном `revisionTimeMs`, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
|
||||||
|
|
||||||
- статус:
|
- статус:
|
||||||
pending
|
pending
|
||||||
|
|||||||
@@ -15,3 +15,4 @@
|
|||||||
|
|
||||||
- код DM и оба документа `Протокол_DM_v1.md` + `Формат_DM_v1.md` всегда должны обновляться синхронно;
|
- код DM и оба документа `Протокол_DM_v1.md` + `Формат_DM_v1.md` всегда должны обновляться синхронно;
|
||||||
- если меняется поведение DM в коде, в том же наборе изменений обновляется и эта документация.
|
- если меняется поведение DM в коде, в том же наборе изменений обновляется и эта документация.
|
||||||
|
- локальная таблица пользователей для DM считается кэшем, а источником истины остаётся Solana PDA; если серверная логика DM меняет правила lazy-import пользователей из PDA, это тоже обязательно фиксируется в документации.
|
||||||
|
|||||||
@@ -71,6 +71,19 @@
|
|||||||
- сервер не должен расшифровывать DM;
|
- сервер не должен расшифровывать DM;
|
||||||
- ciphertext у `type=1` и `type=2` обычно разный и не обязан совпадать побайтно.
|
- ciphertext у `type=1` и `type=2` обычно разный и не обязан совпадать побайтно.
|
||||||
|
|
||||||
|
### 2.3. Источник истины по пользователю
|
||||||
|
|
||||||
|
Для DM-проверки сервер использует:
|
||||||
|
|
||||||
|
- локальную `solana_users` как кэш;
|
||||||
|
- Solana PDA как источник истины по `clientKey` и `access_servers`.
|
||||||
|
|
||||||
|
Если нужного пользователя нет локально, сервер обязан попытаться lazy-import из Solana PDA:
|
||||||
|
|
||||||
|
- в `GetUser`;
|
||||||
|
- при серверной верификации DM-подписи;
|
||||||
|
- перед межсерверной маршрутизацией через `access_servers`.
|
||||||
|
|
||||||
## 3. Типы сообщений
|
## 3. Типы сообщений
|
||||||
|
|
||||||
В версии v1 используются следующие `messageType`:
|
В версии v1 используются следующие `messageType`:
|
||||||
@@ -135,7 +148,8 @@
|
|||||||
- plaintext может остаться тем же;
|
- plaintext может остаться тем же;
|
||||||
- ciphertext меняется;
|
- ciphertext меняется;
|
||||||
- `reencryptedAtMs` получает ненулевое значение;
|
- `reencryptedAtMs` получает ненулевое значение;
|
||||||
- `revisionTimeMs` увеличивается.
|
- `revisionTimeMs` может остаться прежним, в том числе нулевым, если менялось только шифрование без редактирования текста;
|
||||||
|
- сервер при выборе актуальной версии обязан учитывать обе метки времени.
|
||||||
|
|
||||||
Если сообщение уже удалено одним из типов `5/6`, его перешифровывать больше нельзя.
|
Если сообщение уже удалено одним из типов `5/6`, его перешифровывать больше нельзя.
|
||||||
|
|
||||||
@@ -223,7 +237,7 @@
|
|||||||
то сервер:
|
то сервер:
|
||||||
|
|
||||||
- не принимает это сообщение;
|
- не принимает это сообщение;
|
||||||
- в ответ возвращает то же сообщение удаления переписки;
|
- распространяет уже известный tombstone удаления переписки на серверы доступа обеих сторон;
|
||||||
- ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки.
|
- ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки.
|
||||||
|
|
||||||
### 6.5. Будущие новые сообщения
|
### 6.5. Будущие новые сообщения
|
||||||
@@ -281,6 +295,8 @@
|
|||||||
- `SendMessagePair`
|
- `SendMessagePair`
|
||||||
- `ReceiveOutcomingMessage`
|
- `ReceiveOutcomingMessage`
|
||||||
- `ReceiveIncomingMessage`
|
- `ReceiveIncomingMessage`
|
||||||
|
- `DeleteMessage`
|
||||||
|
- `DeleteConversation`
|
||||||
|
|
||||||
Их роли в v1:
|
Их роли в v1:
|
||||||
|
|
||||||
@@ -290,34 +306,27 @@
|
|||||||
|
|
||||||
### 8.2. Новые методы, которые нужны
|
### 8.2. Новые методы, которые нужны
|
||||||
|
|
||||||
Нужны как минимум отдельные операции:
|
Отдельный legacy-метод `SendDirectMessage` в DM v1 не используется и должен оставаться отключённым, чтобы не было параллельного старого стека доставки.
|
||||||
|
|
||||||
- `DeleteMessage`
|
|
||||||
- `DeleteConversation`
|
|
||||||
|
|
||||||
Названия могут быть другими, но логически это две разные операции:
|
|
||||||
|
|
||||||
- удалить одно сообщение у обеих сторон;
|
|
||||||
- удалить всю переписку до заданной временной границы.
|
|
||||||
|
|
||||||
## 9. Правила валидации и применения
|
## 9. Правила валидации и применения
|
||||||
|
|
||||||
### 9.1. Общее правило по ревизиям
|
### 9.1. Общее правило по ревизиям
|
||||||
|
|
||||||
Для одного и того же `messageKey` сервер сравнивает только:
|
Для одного и того же контентного сообщения сервер сравнивает пару:
|
||||||
|
|
||||||
- уже сохранённый `revisionTimeMs`;
|
- `revisionTimeMs`;
|
||||||
- `revisionTimeMs` входящего сообщения.
|
- `reencryptedAtMs`.
|
||||||
|
|
||||||
Правило:
|
Правило:
|
||||||
|
|
||||||
- если новый `revisionTimeMs` больше сохранённого, сообщение применяется;
|
- если новый `revisionTimeMs` больше сохранённого, сообщение применяется;
|
||||||
- если новый `revisionTimeMs` равен сохранённому, сообщение не применяется;
|
- если `revisionTimeMs` равен, но новый `reencryptedAtMs` больше сохранённого, сообщение применяется;
|
||||||
- если новый `revisionTimeMs` меньше сохранённого, сообщение не применяется.
|
- если обе величины равны, сообщение не применяется;
|
||||||
|
- если новая пара (`revisionTimeMs`, `reencryptedAtMs`) меньше или равна сохранённой, сообщение не применяется.
|
||||||
|
|
||||||
Содержимое `body` при этом сравнении не участвует.
|
Содержимое `body` при этом сравнении не участвует.
|
||||||
|
|
||||||
То есть если `revisionTimeMs` совпадает, сервер считает, что такая ревизия у него уже есть.
|
То есть если сервер уже видел ту же пару (`revisionTimeMs`, `reencryptedAtMs`), он считает, что такая версия у него уже есть.
|
||||||
|
|
||||||
### 9.2. Повторный tombstone
|
### 9.2. Повторный tombstone
|
||||||
|
|
||||||
@@ -353,6 +362,8 @@
|
|||||||
- не восстанавливает старую историю;
|
- не восстанавливает старую историю;
|
||||||
- не применяет это событие повторно.
|
- не применяет это событие повторно.
|
||||||
|
|
||||||
|
Если это старое контентное сообщение или входящая копия, сервер дополнительно перерассылает уже известный tombstone удаления переписки на `access_servers` обеих сторон, чтобы отстающие серверы сами подчистили историю.
|
||||||
|
|
||||||
Такие сообщения считаются частью уже удалённой истории.
|
Такие сообщения считаются частью уже удалённой истории.
|
||||||
|
|
||||||
### 9.4. Приоритет удаления переписки
|
### 9.4. Приоритет удаления переписки
|
||||||
@@ -475,6 +486,8 @@ Request:
|
|||||||
|
|
||||||
Если часть серверов совпадает, это допустимо.
|
Если часть серверов совпадает, это допустимо.
|
||||||
|
|
||||||
|
Если один и тот же сервер присутствует у обеих сторон, он не должен слать сообщение сам себе повторно, но обязан локально сохранить событие и доставить его в нужные пользовательские сессии.
|
||||||
|
|
||||||
Идемпотентность обязательна.
|
Идемпотентность обязательна.
|
||||||
|
|
||||||
### 11.3. Ошибки доставки
|
### 11.3. Ошибки доставки
|
||||||
@@ -513,6 +526,9 @@ Request:
|
|||||||
- удаление одного сообщения должно стать терминальным tombstone;
|
- удаление одного сообщения должно стать терминальным tombstone;
|
||||||
- удаление всей переписки должно стать отдельным служебным событием;
|
- удаление всей переписки должно стать отдельным служебным событием;
|
||||||
- межсерверная маршрутизация DM должна идти через `access_servers`;
|
- межсерверная маршрутизация DM должна идти через `access_servers`;
|
||||||
|
- сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM;
|
||||||
|
- при выборе актуальной версии должен учитываться `reencryptedAtMs`, если `revisionTimeMs` совпадает;
|
||||||
|
- legacy `SendDirectMessage` должен быть отключён;
|
||||||
- логика должна быть безопасна для нескольких серверов у каждой стороны.
|
- логика должна быть безопасна для нескольких серверов у каждой стороны.
|
||||||
|
|
||||||
## 14. Что в v1 пока не входит
|
## 14. Что в v1 пока не входит
|
||||||
|
|||||||
@@ -81,11 +81,12 @@ SHiNE_DM
|
|||||||
|
|
||||||
### `revisionTimeMs`
|
### `revisionTimeMs`
|
||||||
|
|
||||||
- `0` для исходной версии сообщения;
|
- `0` для исходной версии контентного сообщения;
|
||||||
- ненулевое значение для новой ревизии сообщения;
|
- ненулевое значение для новой текстовой ревизии сообщения;
|
||||||
- при редактировании или удалении должно быть больше предыдущего значения для этого же `messageKey`.
|
- для типов `5/6` это время tombstone удаления одного сообщения;
|
||||||
|
- для типов `7/8` всегда `0`.
|
||||||
|
|
||||||
То есть это `0` или время, когда сообщение было отредактировано либо удалено.
|
То есть `revisionTimeMs` отвечает именно за изменение смыслового содержимого сообщения, а не за сам факт перешифровки.
|
||||||
|
|
||||||
### `reencryptedAtMs`
|
### `reencryptedAtMs`
|
||||||
|
|
||||||
@@ -94,6 +95,13 @@ SHiNE_DM
|
|||||||
|
|
||||||
Для типов `5/6/7/8` должно быть `0`.
|
Для типов `5/6/7/8` должно быть `0`.
|
||||||
|
|
||||||
|
`reencryptedAtMs` не требует, чтобы `revisionTimeMs` был ненулевым. Допустим сценарий:
|
||||||
|
|
||||||
|
- `revisionTimeMs = 0`
|
||||||
|
- `reencryptedAtMs > 0`
|
||||||
|
|
||||||
|
если исходное сообщение не редактировалось, но было перешифровано позже.
|
||||||
|
|
||||||
### `bodyLen`
|
### `bodyLen`
|
||||||
|
|
||||||
- `> 0` для типов `1/2/3/4`;
|
- `> 0` для типов `1/2/3/4`;
|
||||||
@@ -213,7 +221,7 @@ ReadReceiptBody_v1_0
|
|||||||
|
|
||||||
### Перешифровка
|
### Перешифровка
|
||||||
|
|
||||||
- `revisionTimeMs > 0`
|
- `revisionTimeMs = 0` или `> 0`
|
||||||
- `reencryptedAtMs > 0`
|
- `reencryptedAtMs > 0`
|
||||||
|
|
||||||
## 8. Контент типов `3/4`
|
## 8. Контент типов `3/4`
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public final class SignedMessagesV2DAO {
|
public final class SignedMessagesV2DAO {
|
||||||
|
public enum ApplyStatus {
|
||||||
|
APPLIED,
|
||||||
|
DUPLICATE_OR_OLDER,
|
||||||
|
BLOCKED_BY_MESSAGE_TOMBSTONE,
|
||||||
|
BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||||
|
|
||||||
|
public boolean applied() {
|
||||||
|
return this == APPLIED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static volatile SignedMessagesV2DAO instance;
|
private static volatile SignedMessagesV2DAO instance;
|
||||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||||
|
|
||||||
@@ -25,8 +36,11 @@ public final class SignedMessagesV2DAO {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean insertIfAbsent(SignedMessageV2Entry e) throws Exception {
|
public ApplyStatus insertIfAbsent(SignedMessageV2Entry e) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
|
if (isBlockedByConversationDelete(c, e.getFromLogin(), e.getToLogin(), e.getTimeMs())) {
|
||||||
|
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||||
|
}
|
||||||
String sql = """
|
String sql = """
|
||||||
INSERT OR IGNORE INTO signed_messages_v2 (
|
INSERT OR IGNORE INTO signed_messages_v2 (
|
||||||
message_key, base_key, target_login, from_login, to_login,
|
message_key, base_key, target_login, from_login, to_login,
|
||||||
@@ -37,7 +51,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
bindSignedMessage(ps, e);
|
bindSignedMessage(ps, e);
|
||||||
return ps.executeUpdate() > 0;
|
return ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,25 +81,25 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
|
public ApplyStatus upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
try {
|
try {
|
||||||
if (isBlockedByConversationDelete(c, incoming.getFromLogin(), incoming.getToLogin(), incoming.getTimeMs())) {
|
if (isBlockedByConversationDelete(c, incoming.getFromLogin(), incoming.getToLogin(), incoming.getTimeMs())) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||||
}
|
}
|
||||||
if (hasMessageDeleteTombstone(c, incoming.getBaseKey())) {
|
if (hasMessageDeleteTombstone(c, incoming.getBaseKey())) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.BLOCKED_BY_MESSAGE_TOMBSTONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
Long currentRevision = getCurrentContentRevision(c, incoming.getBaseKey());
|
RevisionMarker currentMarker = getCurrentContentMarker(c, incoming.getBaseKey());
|
||||||
long nextRevision = incoming.getRevisionTimeMs();
|
RevisionMarker nextMarker = RevisionMarker.of(incoming);
|
||||||
if (currentRevision != null && nextRevision <= currentRevision) {
|
if (currentMarker != null && compareMarkers(nextMarker, currentMarker) <= 0) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertMessage(c, incoming);
|
upsertMessage(c, incoming);
|
||||||
@@ -94,7 +108,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
resetDeliveryRows(c, outgoing.getMessageKey());
|
resetDeliveryRows(c, outgoing.getMessageKey());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return true;
|
return ApplyStatus.APPLIED;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -104,31 +118,31 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception {
|
public ApplyStatus upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
try {
|
try {
|
||||||
if (isBlockedByConversationDelete(c, incoming.getFromLogin(), incoming.getToLogin(), incoming.getTimeMs())) {
|
if (isBlockedByConversationDelete(c, incoming.getFromLogin(), incoming.getToLogin(), incoming.getTimeMs())) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||||
}
|
}
|
||||||
if (hasMessageDeleteTombstone(c, incoming.getBaseKey())) {
|
if (hasMessageDeleteTombstone(c, incoming.getBaseKey())) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.BLOCKED_BY_MESSAGE_TOMBSTONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
Long currentRevision = getRevisionTimeMs(c, incoming.getMessageKey());
|
RevisionMarker currentMarker = getRevisionMarkerByMessageKey(c, incoming.getMessageKey());
|
||||||
long nextRevision = incoming.getRevisionTimeMs();
|
RevisionMarker nextMarker = RevisionMarker.of(incoming);
|
||||||
if (currentRevision != null && nextRevision <= currentRevision) {
|
if (currentMarker != null && compareMarkers(nextMarker, currentMarker) <= 0) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertMessage(c, incoming);
|
upsertMessage(c, incoming);
|
||||||
resetDeliveryRows(c, incoming.getMessageKey());
|
resetDeliveryRows(c, incoming.getMessageKey());
|
||||||
c.commit();
|
c.commit();
|
||||||
return true;
|
return ApplyStatus.APPLIED;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -138,18 +152,18 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception {
|
public ApplyStatus applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
try {
|
try {
|
||||||
if (isBlockedByConversationDelete(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs())) {
|
if (isBlockedByConversationDelete(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs())) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||||
}
|
}
|
||||||
if (hasMessageDeleteTombstone(c, tombstone.getBaseKey())) {
|
if (hasMessageDeleteTombstone(c, tombstone.getBaseKey())) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMessageContentAndReceipts(c, tombstone.getBaseKey());
|
deleteMessageContentAndReceipts(c, tombstone.getBaseKey());
|
||||||
@@ -157,7 +171,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return true;
|
return ApplyStatus.APPLIED;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -167,7 +181,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception {
|
public ApplyStatus applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
boolean prevAutoCommit = c.getAutoCommit();
|
boolean prevAutoCommit = c.getAutoCommit();
|
||||||
c.setAutoCommit(false);
|
c.setAutoCommit(false);
|
||||||
@@ -175,7 +189,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
Long currentBoundary = getLatestConversationDeleteBoundary(c, tombstone.getFromLogin(), tombstone.getToLogin());
|
Long currentBoundary = getLatestConversationDeleteBoundary(c, tombstone.getFromLogin(), tombstone.getToLogin());
|
||||||
if (currentBoundary != null && tombstone.getTimeMs() <= currentBoundary) {
|
if (currentBoundary != null && tombstone.getTimeMs() <= currentBoundary) {
|
||||||
c.rollback();
|
c.rollback();
|
||||||
return false;
|
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteConversationHistoryBefore(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs());
|
deleteConversationHistoryBefore(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs());
|
||||||
@@ -183,7 +197,7 @@ public final class SignedMessagesV2DAO {
|
|||||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||||
|
|
||||||
c.commit();
|
c.commit();
|
||||||
return true;
|
return ApplyStatus.APPLIED;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
try { c.rollback(); } catch (Exception ignored) {}
|
try { c.rollback(); } catch (Exception ignored) {}
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -214,6 +228,12 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SignedMessageV2Entry getLatestConversationDelete(String fromLogin, String toLogin) throws Exception {
|
||||||
|
try (Connection c = db.getConnection()) {
|
||||||
|
return getLatestConversationDelete(c, fromLogin, toLogin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ensureDeliveryRow(String messageKey, String sessionId, long nowMs) throws Exception {
|
public void ensureDeliveryRow(String messageKey, String sessionId, long nowMs) throws Exception {
|
||||||
try (Connection c = db.getConnection()) {
|
try (Connection c = db.getConnection()) {
|
||||||
String sql = """
|
String sql = """
|
||||||
@@ -338,30 +358,36 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getRevisionTimeMs(Connection c, String messageKey) throws SQLException {
|
private RevisionMarker getRevisionMarkerByMessageKey(Connection c, String messageKey) throws SQLException {
|
||||||
String sql = "SELECT revision_time_ms FROM signed_messages_v2 WHERE message_key = ? LIMIT 1";
|
String sql = """
|
||||||
|
SELECT revision_time_ms, reencrypted_at_ms
|
||||||
|
FROM signed_messages_v2
|
||||||
|
WHERE message_key = ?
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, messageKey);
|
ps.setString(1, messageKey);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (!rs.next()) return null;
|
if (!rs.next()) return null;
|
||||||
return rs.getLong(1);
|
return new RevisionMarker(rs.getLong(1), rs.getLong(2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getCurrentContentRevision(Connection c, String baseKey) throws SQLException {
|
private RevisionMarker getCurrentContentMarker(Connection c, String baseKey) throws SQLException {
|
||||||
String sql = """
|
String sql = """
|
||||||
SELECT MAX(revision_time_ms)
|
SELECT revision_time_ms, reencrypted_at_ms
|
||||||
FROM signed_messages_v2
|
FROM signed_messages_v2
|
||||||
WHERE base_key = ?
|
WHERE base_key = ?
|
||||||
AND message_type IN (1, 2)
|
AND message_type IN (1, 2)
|
||||||
|
ORDER BY revision_time_ms DESC, reencrypted_at_ms DESC
|
||||||
|
LIMIT 1
|
||||||
""";
|
""";
|
||||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
ps.setString(1, baseKey);
|
ps.setString(1, baseKey);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
if (!rs.next()) return null;
|
if (!rs.next()) return null;
|
||||||
long value = rs.getLong(1);
|
return new RevisionMarker(rs.getLong(1), rs.getLong(2));
|
||||||
return rs.wasNull() ? null : value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +436,34 @@ public final class SignedMessagesV2DAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SignedMessageV2Entry getLatestConversationDelete(Connection c, String fromLogin, String toLogin) throws Exception {
|
||||||
|
String sql = """
|
||||||
|
SELECT
|
||||||
|
message_key, base_key, target_login, from_login, to_login,
|
||||||
|
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||||
|
raw_block, created_at_ms, source_api, origin_session_id,
|
||||||
|
receipt_ref_base_key, receipt_ref_type
|
||||||
|
FROM signed_messages_v2
|
||||||
|
WHERE message_type IN (7, 8)
|
||||||
|
AND (
|
||||||
|
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||||
|
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||||
|
)
|
||||||
|
ORDER BY time_ms DESC
|
||||||
|
LIMIT 1
|
||||||
|
""";
|
||||||
|
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, fromLogin);
|
||||||
|
ps.setString(2, toLogin);
|
||||||
|
ps.setString(3, toLogin);
|
||||||
|
ps.setString(4, fromLogin);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
if (!rs.next()) return null;
|
||||||
|
return mapRow(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void deleteMessageContentAndReceipts(Connection c, String baseKey) throws SQLException {
|
private void deleteMessageContentAndReceipts(Connection c, String baseKey) throws SQLException {
|
||||||
deleteDeliveryRowsByMessageSelection(c, """
|
deleteDeliveryRowsByMessageSelection(c, """
|
||||||
SELECT message_key
|
SELECT message_key
|
||||||
@@ -532,6 +586,12 @@ public final class SignedMessagesV2DAO {
|
|||||||
return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
|
return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int compareMarkers(RevisionMarker left, RevisionMarker right) {
|
||||||
|
int revisionCompare = Long.compare(left.revisionTimeMs, right.revisionTimeMs);
|
||||||
|
if (revisionCompare != 0) return revisionCompare;
|
||||||
|
return Long.compare(left.reencryptedAtMs, right.reencryptedAtMs);
|
||||||
|
}
|
||||||
|
|
||||||
private SignedMessageV2Entry mapRow(ResultSet rs) throws Exception {
|
private SignedMessageV2Entry mapRow(ResultSet rs) throws Exception {
|
||||||
SignedMessageV2Entry e = new SignedMessageV2Entry();
|
SignedMessageV2Entry e = new SignedMessageV2Entry();
|
||||||
e.setMessageKey(rs.getString("message_key"));
|
e.setMessageKey(rs.getString("message_key"));
|
||||||
@@ -553,4 +613,10 @@ public final class SignedMessagesV2DAO {
|
|||||||
e.setReceiptRefType(rs.wasNull() ? null : maybeRefType);
|
e.setReceiptRefType(rs.wasNull() ? null : maybeRefType);
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RevisionMarker(long revisionTimeMs, long reencryptedAtMs) {
|
||||||
|
private static RevisionMarker of(SignedMessageV2Entry entry) {
|
||||||
|
return new RevisionMarker(entry.getRevisionTimeMs(), entry.getReencryptedAtMs());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -195,7 +195,6 @@ public final class JsonHandlerRegistry {
|
|||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
||||||
Map.entry("SendTestWebPush", new Net_SendTestWebPush_Handler()),
|
Map.entry("SendTestWebPush", new Net_SendTestWebPush_Handler()),
|
||||||
Map.entry("SendDirectMessage", new Net_SendDirectMessage_Handler()),
|
|
||||||
Map.entry("SendMessagePair", new Net_SendMessagePair_Handler()),
|
Map.entry("SendMessagePair", new Net_SendMessagePair_Handler()),
|
||||||
Map.entry("ReceiveOutcomingMessage", new Net_SendMessagePair_Handler()),
|
Map.entry("ReceiveOutcomingMessage", new Net_SendMessagePair_Handler()),
|
||||||
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
||||||
@@ -276,7 +275,6 @@ public final class JsonHandlerRegistry {
|
|||||||
// --- direct messages / push ---
|
// --- direct messages / push ---
|
||||||
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
||||||
Map.entry("SendTestWebPush", Net_SendTestWebPush_Request.class),
|
Map.entry("SendTestWebPush", Net_SendTestWebPush_Request.class),
|
||||||
Map.entry("SendDirectMessage", Net_SendDirectMessage_Request.class),
|
|
||||||
Map.entry("SendMessagePair", Net_SendMessagePair_Request.class),
|
Map.entry("SendMessagePair", Net_SendMessagePair_Request.class),
|
||||||
Map.entry("ReceiveOutcomingMessage", Net_SendMessagePair_Request.class),
|
Map.entry("ReceiveOutcomingMessage", Net_SendMessagePair_Request.class),
|
||||||
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
||||||
|
|||||||
+4
@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
||||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
|
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
|
||||||
@@ -42,6 +43,9 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
|
SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
|
||||||
|
if (u == null) {
|
||||||
|
u = SolanaUserPdaImportService.findOrImportByLogin(req.getLogin());
|
||||||
|
}
|
||||||
|
|
||||||
Net_GetUser_Response resp = new Net_GetUser_Response();
|
Net_GetUser_Response resp = new Net_GetUser_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
|
|||||||
+3
-3
@@ -39,13 +39,13 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteConversation", null);
|
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteConversation", null);
|
||||||
boolean applied = SignedMessagesV2DAO.getInstance().applyDeleteConversation(entry);
|
SignedMessagesV2DAO.ApplyStatus status = SignedMessagesV2DAO.getInstance().applyDeleteConversation(entry);
|
||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (applied) {
|
if (status.applied()) {
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (applied && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
DmFederationService.fanOutDeleteConversation(block.fromLogin, block.toLogin, block.messageType, req.getBlobB64().trim());
|
DmFederationService.fanOutDeleteConversation(block.fromLogin, block.toLogin, block.messageType, req.getBlobB64().trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -39,13 +39,13 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteMessage", null);
|
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteMessage", null);
|
||||||
boolean applied = SignedMessagesV2DAO.getInstance().applyDeleteMessage(entry);
|
SignedMessagesV2DAO.ApplyStatus status = SignedMessagesV2DAO.getInstance().applyDeleteMessage(entry);
|
||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (applied) {
|
if (status.applied()) {
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||||
}
|
}
|
||||||
if (applied && ctx != null && ctx.isAuthenticatedUser()) {
|
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
DmFederationService.fanOutDeleteMessage(block.fromLogin, block.toLogin, block.messageType, req.getBlobB64().trim());
|
DmFederationService.fanOutDeleteMessage(block.fromLogin, block.toLogin, block.messageType, req.getBlobB64().trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -11,6 +11,8 @@ import server.logic.ws_protocol.WireCodes;
|
|||||||
import shine.db.dao.SignedMessagesV2DAO;
|
import shine.db.dao.SignedMessagesV2DAO;
|
||||||
import shine.db.entities.SignedMessageV2Entry;
|
import shine.db.entities.SignedMessageV2Entry;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||||
@Override
|
@Override
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||||
@@ -45,13 +47,16 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean inserted = incoming.isContentType()
|
SignedMessagesV2DAO.ApplyStatus status = incoming.isContentType()
|
||||||
? SignedMessagesV2DAO.getInstance().upsertIncomingCopy(entry)
|
? SignedMessagesV2DAO.getInstance().upsertIncomingCopy(entry)
|
||||||
: SignedMessagesV2DAO.getInstance().insertIfAbsent(entry);
|
: SignedMessagesV2DAO.getInstance().insertIfAbsent(entry);
|
||||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (inserted) {
|
if (status.applied()) {
|
||||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
||||||
}
|
}
|
||||||
|
if (status == SignedMessagesV2DAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||||
|
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||||
|
}
|
||||||
|
|
||||||
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
||||||
resp.setOp(req.getOp());
|
resp.setOp(req.getOp());
|
||||||
@@ -67,4 +72,15 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
|||||||
private boolean isBlank(String s) {
|
private boolean isBlank(String s) {
|
||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bounceConversationDeleteIfKnown(String fromLogin, String toLogin) throws Exception {
|
||||||
|
SignedMessageV2Entry tombstone = SignedMessagesV2DAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||||
|
if (tombstone == null || tombstone.getRawBlock() == null || tombstone.getRawBlock().length == 0) return;
|
||||||
|
server.sync.DmFederationService.fanOutDeleteConversation(
|
||||||
|
tombstone.getFromLogin(),
|
||||||
|
tombstone.getToLogin(),
|
||||||
|
tombstone.getMessageType(),
|
||||||
|
Base64.getEncoder().encodeToString(tombstone.getRawBlock())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-6
@@ -12,6 +12,8 @@ import server.sync.DmFederationService;
|
|||||||
import shine.db.dao.SignedMessagesV2DAO;
|
import shine.db.dao.SignedMessagesV2DAO;
|
||||||
import shine.db.entities.SignedMessageV2Entry;
|
import shine.db.entities.SignedMessageV2Entry;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||||
@Override
|
@Override
|
||||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||||
@@ -50,17 +52,19 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean pairInserted;
|
SignedMessagesV2DAO.ApplyStatus pairStatus;
|
||||||
if (incoming.isContentType()) {
|
if (incoming.isContentType()) {
|
||||||
pairInserted = SignedMessagesV2DAO.getInstance().upsertContentPair(
|
pairStatus = SignedMessagesV2DAO.getInstance().upsertContentPair(
|
||||||
incomingEntry, outgoingEntry
|
incomingEntry, outgoingEntry
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
pairInserted = SignedMessagesV2DAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry);
|
pairStatus = SignedMessagesV2DAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry)
|
||||||
|
? SignedMessagesV2DAO.ApplyStatus.APPLIED
|
||||||
|
: SignedMessagesV2DAO.ApplyStatus.DUPLICATE_OR_OLDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
SignedMessagesRealtime.DeliveryCounters inCounters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters inCounters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (pairInserted) {
|
if (pairStatus.applied()) {
|
||||||
inCounters = SignedMessagesRealtime.deliverToRelevantSessions(incomingEntry, incoming);
|
inCounters = SignedMessagesRealtime.deliverToRelevantSessions(incomingEntry, incoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,11 +73,15 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
excludeSessionId = ctx.getSessionId();
|
excludeSessionId = ctx.getSessionId();
|
||||||
}
|
}
|
||||||
SignedMessagesRealtime.DeliveryCounters outCounters = new SignedMessagesRealtime.DeliveryCounters();
|
SignedMessagesRealtime.DeliveryCounters outCounters = new SignedMessagesRealtime.DeliveryCounters();
|
||||||
if (pairInserted) {
|
if (pairStatus.applied()) {
|
||||||
outCounters = SignedMessagesRealtime.deliverToRelevantSessions(outgoingEntry, outgoing, excludeSessionId);
|
outCounters = SignedMessagesRealtime.deliverToRelevantSessions(outgoingEntry, outgoing, excludeSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pairInserted && ctx != null && ctx.isAuthenticatedUser()) {
|
if (pairStatus == SignedMessagesV2DAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||||
|
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pairStatus.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||||
DmFederationService.fanOutPair(
|
DmFederationService.fanOutPair(
|
||||||
incoming.fromLogin,
|
incoming.fromLogin,
|
||||||
incoming.toLogin,
|
incoming.toLogin,
|
||||||
@@ -97,4 +105,15 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
|||||||
private boolean isBlank(String s) {
|
private boolean isBlank(String s) {
|
||||||
return s == null || s.isBlank();
|
return s == null || s.isBlank();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bounceConversationDeleteIfKnown(String fromLogin, String toLogin) throws Exception {
|
||||||
|
SignedMessageV2Entry tombstone = SignedMessagesV2DAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||||
|
if (tombstone == null || tombstone.getRawBlock() == null || tombstone.getRawBlock().length == 0) return;
|
||||||
|
DmFederationService.fanOutDeleteConversation(
|
||||||
|
tombstone.getFromLogin(),
|
||||||
|
tombstone.getToLogin(),
|
||||||
|
tombstone.getMessageType(),
|
||||||
|
Base64.getEncoder().encodeToString(tombstone.getRawBlock())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-6
@@ -116,12 +116,6 @@ final class SignedMessageBlock {
|
|||||||
if (messageType == TYPE_INCOMING_TEXT || messageType == TYPE_OUTGOING_COPY) {
|
if (messageType == TYPE_INCOMING_TEXT || messageType == TYPE_OUTGOING_COPY) {
|
||||||
if (bodyBytes.length == 0) throw new IllegalArgumentException("BAD_MESSAGE_LEN");
|
if (bodyBytes.length == 0) throw new IllegalArgumentException("BAD_MESSAGE_LEN");
|
||||||
encryptedBody = EncryptedBodyPayload.parse(bodyBytes, maxBodyBytes);
|
encryptedBody = EncryptedBodyPayload.parse(bodyBytes, maxBodyBytes);
|
||||||
if (revisionTimeMs == 0 && reencryptedAtMs != 0) {
|
|
||||||
throw new IllegalArgumentException("BAD_REENCRYPTED_TIME");
|
|
||||||
}
|
|
||||||
if (reencryptedAtMs > 0 && revisionTimeMs == 0) {
|
|
||||||
throw new IllegalArgumentException("BAD_REENCRYPTED_TIME");
|
|
||||||
}
|
|
||||||
} else if (messageType == TYPE_READ_INCOMING || messageType == TYPE_READ_OUTGOING_COPY) {
|
} else if (messageType == TYPE_READ_INCOMING || messageType == TYPE_READ_OUTGOING_COPY) {
|
||||||
if (bodyBytes.length == 0) throw new IllegalArgumentException("BAD_RECEIPT_PAYLOAD_LEN");
|
if (bodyBytes.length == 0) throw new IllegalArgumentException("BAD_RECEIPT_PAYLOAD_LEN");
|
||||||
if (revisionTimeMs != 0 || reencryptedAtMs != 0) {
|
if (revisionTimeMs != 0 || reencryptedAtMs != 0) {
|
||||||
|
|||||||
+10
-2
@@ -1,5 +1,6 @@
|
|||||||
package server.logic.ws_protocol.JSON.messages;
|
package server.logic.ws_protocol.JSON.messages;
|
||||||
|
|
||||||
|
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
||||||
import shine.db.dao.SolanaUsersDAO;
|
import shine.db.dao.SolanaUsersDAO;
|
||||||
import shine.db.entities.SignedMessageV2Entry;
|
import shine.db.entities.SignedMessageV2Entry;
|
||||||
import shine.db.entities.SolanaUserEntry;
|
import shine.db.entities.SolanaUserEntry;
|
||||||
@@ -46,8 +47,8 @@ final class SignedMessagesCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void verifyUsersAndSignature(SignedMessageBlock block) throws Exception {
|
static void verifyUsersAndSignature(SignedMessageBlock block) throws Exception {
|
||||||
SolanaUserEntry from = SolanaUsersDAO.getInstance().getByLogin(block.fromLogin);
|
SolanaUserEntry from = resolveUser(block.fromLogin);
|
||||||
SolanaUserEntry to = SolanaUsersDAO.getInstance().getByLogin(block.toLogin);
|
SolanaUserEntry to = resolveUser(block.toLogin);
|
||||||
if (from == null || to == null) {
|
if (from == null || to == null) {
|
||||||
throw new IllegalArgumentException("USER_NOT_FOUND");
|
throw new IllegalArgumentException("USER_NOT_FOUND");
|
||||||
}
|
}
|
||||||
@@ -60,6 +61,13 @@ final class SignedMessagesCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SolanaUserEntry resolveUser(String login) throws Exception {
|
||||||
|
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||||
|
SolanaUserEntry user = usersDao.getByLogin(login);
|
||||||
|
if (user != null) return user;
|
||||||
|
return SolanaUserPdaImportService.findOrImportByLogin(login);
|
||||||
|
}
|
||||||
|
|
||||||
static void validatePair(SignedMessageBlock incoming, SignedMessageBlock outgoing) {
|
static void validatePair(SignedMessageBlock incoming, SignedMessageBlock outgoing) {
|
||||||
if (incoming.messageType % 2 == 0) throw new IllegalArgumentException("BAD_PAIR_TYPES");
|
if (incoming.messageType % 2 == 0) throw new IllegalArgumentException("BAD_PAIR_TYPES");
|
||||||
if (outgoing.messageType != incoming.messageType + 1) throw new IllegalArgumentException("BAD_PAIR_TYPES");
|
if (outgoing.messageType != incoming.messageType + 1) throw new IllegalArgumentException("BAD_PAIR_TYPES");
|
||||||
|
|||||||
+2
-2
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.299
|
client.version=1.2.300
|
||||||
server.version=1.2.279
|
server.version=1.2.280
|
||||||
|
|||||||
+1
-1
@@ -1088,7 +1088,7 @@ async function init() {
|
|||||||
let refBaseKey = String(payload.receiptRefBaseKey || '').trim();
|
let refBaseKey = String(payload.receiptRefBaseKey || '').trim();
|
||||||
if (!refBaseKey) {
|
if (!refBaseKey) {
|
||||||
try {
|
try {
|
||||||
const ref = authService.parseReadReceiptPayload(parsed.payloadBytes);
|
const ref = parsed.readReceiptPayload || authService.parseReadReceiptPayload(parsed.payloadBytes);
|
||||||
refBaseKey = `${ref.refFromLogin}|${ref.refToLogin}|${ref.refTimeMs}|${ref.refNonce}`;
|
refBaseKey = `${ref.refFromLogin}|${ref.refToLogin}|${ref.refTimeMs}|${ref.refNonce}`;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,6 +330,42 @@ function parseEncryptedDmBodyBytes(payloadBytes) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseReadReceiptBodyBytes(payloadBytes) {
|
||||||
|
if (!(payloadBytes instanceof Uint8Array)) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||||
|
let o = 0;
|
||||||
|
const read = (n) => {
|
||||||
|
if (o + n > payloadBytes.length) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||||
|
const out = payloadBytes.slice(o, o + n);
|
||||||
|
o += n;
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
const readU8 = () => read(1)[0];
|
||||||
|
const readU32 = () => {
|
||||||
|
const part = read(4);
|
||||||
|
return new DataView(part.buffer, part.byteOffset, 4).getUint32(0, false);
|
||||||
|
};
|
||||||
|
const readU64 = () => {
|
||||||
|
const part = read(8);
|
||||||
|
return Number(new DataView(part.buffer, part.byteOffset, 8).getBigUint64(0, false));
|
||||||
|
};
|
||||||
|
const readAscii = (code) => {
|
||||||
|
const len = readU8();
|
||||||
|
const part = read(len);
|
||||||
|
for (let i = 0; i < part.length; i += 1) {
|
||||||
|
const c = part[i];
|
||||||
|
if (c < 0x20 || c > 0x7e) throw new Error(code);
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(part);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refToLogin = readAscii('BAD_RECEIPT_TO_LOGIN');
|
||||||
|
const refFromLogin = readAscii('BAD_RECEIPT_FROM_LOGIN');
|
||||||
|
const refTimeMs = readU64();
|
||||||
|
const refNonce = readU32();
|
||||||
|
if (o !== payloadBytes.length) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||||
|
return { refToLogin, refFromLogin, refTimeMs, refNonce };
|
||||||
|
}
|
||||||
|
|
||||||
function parseSignedMessageBlockBytes(bytes) {
|
function parseSignedMessageBlockBytes(bytes) {
|
||||||
if (!(bytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
if (!(bytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||||
let o = 0;
|
let o = 0;
|
||||||
@@ -374,11 +410,17 @@ function parseSignedMessageBlockBytes(bytes) {
|
|||||||
read(DM_PREFIX_V1.length);
|
read(DM_PREFIX_V1.length);
|
||||||
const formatVersionMajor = readU8();
|
const formatVersionMajor = readU8();
|
||||||
const formatVersionMinor = readU8();
|
const formatVersionMinor = readU8();
|
||||||
|
if (formatVersionMajor !== DM_FORMAT_VERSION_MAJOR || formatVersionMinor !== DM_FORMAT_VERSION_MINOR) {
|
||||||
|
throw new Error('BAD_FORMAT_VERSION');
|
||||||
|
}
|
||||||
const toLogin = readAscii();
|
const toLogin = readAscii();
|
||||||
const fromLogin = readAscii();
|
const fromLogin = readAscii();
|
||||||
const timeMs = readU64();
|
const timeMs = readU64();
|
||||||
const nonce = readU32();
|
const nonce = readU32();
|
||||||
const messageType = readU8();
|
const messageType = readU8();
|
||||||
|
if (messageType < DM_TYPE_INCOMING || messageType > DM_TYPE_CONVERSATION_DELETED_BY_RECIPIENT) {
|
||||||
|
throw new Error('BAD_MESSAGE_TYPE');
|
||||||
|
}
|
||||||
const revisionTimeMs = readU64();
|
const revisionTimeMs = readU64();
|
||||||
const reencryptedAtMs = readU64();
|
const reencryptedAtMs = readU64();
|
||||||
const bodyLen = readU32();
|
const bodyLen = readU32();
|
||||||
@@ -392,9 +434,20 @@ function parseSignedMessageBlockBytes(bytes) {
|
|||||||
let encryptedBodyPayload = null;
|
let encryptedBodyPayload = null;
|
||||||
let readReceiptPayload = null;
|
let readReceiptPayload = null;
|
||||||
if (messageType === DM_TYPE_INCOMING || messageType === DM_TYPE_OUTGOING_COPY) {
|
if (messageType === DM_TYPE_INCOMING || messageType === DM_TYPE_OUTGOING_COPY) {
|
||||||
|
if (bodyBytes.length === 0) throw new Error('BAD_MESSAGE_LEN');
|
||||||
encryptedBodyPayload = parseEncryptedDmBodyBytes(bodyBytes);
|
encryptedBodyPayload = parseEncryptedDmBodyBytes(bodyBytes);
|
||||||
} else if (messageType === DM_TYPE_READ_INCOMING || messageType === DM_TYPE_READ_OUTGOING_COPY) {
|
} else if (messageType === DM_TYPE_READ_INCOMING || messageType === DM_TYPE_READ_OUTGOING_COPY) {
|
||||||
readReceiptPayload = null;
|
if (bodyBytes.length === 0) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||||
|
if (revisionTimeMs !== 0 || reencryptedAtMs !== 0) throw new Error('BAD_RECEIPT_REVISION');
|
||||||
|
readReceiptPayload = parseReadReceiptBodyBytes(bodyBytes);
|
||||||
|
} else if (messageType === DM_TYPE_MESSAGE_DELETED_BY_SENDER || messageType === DM_TYPE_MESSAGE_DELETED_BY_RECIPIENT) {
|
||||||
|
if (bodyBytes.length !== 0) throw new Error('BAD_DELETE_BODY');
|
||||||
|
if (revisionTimeMs <= 0) throw new Error('BAD_REVISION_TIME');
|
||||||
|
if (reencryptedAtMs !== 0) throw new Error('BAD_REENCRYPTED_TIME');
|
||||||
|
} else if (messageType === DM_TYPE_CONVERSATION_DELETED_BY_SENDER || messageType === DM_TYPE_CONVERSATION_DELETED_BY_RECIPIENT) {
|
||||||
|
if (bodyBytes.length !== 0) throw new Error('BAD_DELETE_BODY');
|
||||||
|
if (revisionTimeMs !== 0) throw new Error('BAD_REVISION_TIME');
|
||||||
|
if (reencryptedAtMs !== 0) throw new Error('BAD_REENCRYPTED_TIME');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -2278,34 +2331,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseReadReceiptPayload(payloadBytes) {
|
parseReadReceiptPayload(payloadBytes) {
|
||||||
if (!(payloadBytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
return parseReadReceiptBodyBytes(payloadBytes);
|
||||||
let o = 0;
|
|
||||||
const read = (n) => {
|
|
||||||
if (o + n > payloadBytes.length) throw new Error('BAD_RECEIPT_LEN');
|
|
||||||
const out = payloadBytes.slice(o, o + n);
|
|
||||||
o += n;
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
const readU8 = () => read(1)[0];
|
|
||||||
const readU32 = () => {
|
|
||||||
const part = read(4);
|
|
||||||
return new DataView(part.buffer, part.byteOffset, 4).getUint32(0, false);
|
|
||||||
};
|
|
||||||
const readU64 = () => {
|
|
||||||
const part = read(8);
|
|
||||||
return Number(new DataView(part.buffer, part.byteOffset, 8).getBigUint64(0, false));
|
|
||||||
};
|
|
||||||
const readAscii = () => {
|
|
||||||
const len = readU8();
|
|
||||||
const part = read(len);
|
|
||||||
return new TextDecoder().decode(part);
|
|
||||||
};
|
|
||||||
const refToLogin = readAscii();
|
|
||||||
const refFromLogin = readAscii();
|
|
||||||
const refTimeMs = readU64();
|
|
||||||
const refNonce = readU32();
|
|
||||||
if (o !== payloadBytes.length) throw new Error('BAD_RECEIPT_LEN');
|
|
||||||
return { refToLogin, refFromLogin, refTimeMs, refNonce };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptSignedMessageContent({ parsed, blobB64 = '', login, storagePwd }) {
|
async decryptSignedMessageContent({ parsed, blobB64 = '', login, storagePwd }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user