17 KiB
API для разработчиков: DM, push и сигналы звонков
Документ описывает публичные операции, связанные с личными сообщениями, WebPush и сигналами звонков.
Подробная логика DM и бинарного формата:
docs/Personal_Messages/Протокол_DM_v1.mddocs/Personal_Messages/Формат_DM_v1.md
Важно:
- для DM v1 нужно использовать
SendMessagePair,ReceiveOutcomingMessage,ReceiveIncomingMessage,DeleteMessage,DeleteConversation,GetDirectMessages; DmSyncBatchпредназначен для межсерверной догоняющей синхронизации, не для обычного клиентского UI.- сервер поддерживает материализованный слой диалогов
dm_dialog_state;read receiptобновляет серверный watermark иunreadCount, а не только локальный клиентский флаг. - в
dm_dialog_stateсервер также хранитlast_message_blob_b64для последнего контентного DM в base64, чтобы клиент мог отрисовать список чатов без дополнительного запроса.
1. UpsertPushToken
Требует авторизации.
Запрос
{
"op": "UpsertPushToken",
"requestId": "push-upsert-001",
"payload": {
"sessionId": "SESSION_ID",
"endpoint": "https://push.example/...",
"p256dhKey": "BASE64",
"authKey": "BASE64",
"platform": "web",
"userAgent": "Mozilla/5.0 ..."
}
}
Успешный ответ
{
"op": "UpsertPushToken",
"requestId": "push-upsert-001",
"status": 200,
"ok": true,
"payload": {
"tokenId": "token-1",
"updatedAtMs": 1774700000123
}
}
2. SendTestWebPush
Требует авторизации.
Запрос
{
"op": "SendTestWebPush",
"requestId": "push-test-001",
"payload": {
"login": "alice",
"sessionId": "SESSION_ID",
"title": "Test",
"text": "Push body"
}
}
3. SendMessagePair и ReceiveOutcomingMessage
ReceiveOutcomingMessage — алиас SendMessagePair.
Назначение
Передаёт пару signed DM-блоков:
incomingBlobB64— блокtype=1илиtype=3outgoingBlobB64— блокtype=2илиtype=4
Все типы в этой паре используют бинарный формат SHiNE_DM.
Запрос
{
"op": "SendMessagePair",
"requestId": "dm-pair-001",
"payload": {
"incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK",
"outgoingBlobB64": "BASE64_OUTGOING_SIGNED_BLOCK"
}
}
Успешный ответ
{
"op": "SendMessagePair",
"requestId": "dm-pair-001",
"status": 200,
"ok": true,
"payload": {
"baseKey": "from|to|time|nonce",
"incomingKey": "from|to|time|nonce|1",
"outgoingKey": "from|to|time|nonce|2",
"deliveryState": "accepted",
"deliveredWsSessions": 1,
"deliveredWebPushSessions": 0
}
}
Успешный status=200 подтверждает локальное сохранение. До ответа клиенту сервер параллельно пробует оба актуальных сервера получателя. Поэтому deliveryState уже может быть delivered; если никто не ответил, возвращается accepted.
Возможные deliveryState: accepted, delivered, failed.
Ошибки
400 / BAD_FIELDS— пустойincomingBlobB64илиoutgoingBlobB64400 / BAD_BLOCK_FORMAT— base64 или бинарный контейнер повреждён400 / BAD_CRYPTO_METHOD— неподдерживаемый метод шифрования контейнераEncryptedBody_v1_0404 / USER_NOT_FOUND— один из логинов не найден даже после попытки lazy-import из Solana PDA460 / BAD_SIGNATURE— подпись блока не прошла проверку
4. ReceiveIncomingMessage
Принимает только один входящий signed DM-блок.
Назначение
Используется там, где нужно принять только incoming-вариант сообщения.
Принимаемые типы:
type=1— входящая копия сообщенияtype=3— входящий read-receipt
Запрос
{
"op": "ReceiveIncomingMessage",
"requestId": "dm-in-001",
"payload": {
"incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK",
"sourceServerLogin": "server-a"
}
}
sourceServerLogin необязателен для совместимости. Пока межсерверная авторизация отложена, это поле считается доверенным. Пользовательская подпись signed-блока проверяется всегда. Пустой sourceServerLogin трактуется как клиентский вызов, непустой - как peer-вызов.
Успешный ответ содержит существующие messageKey, baseKey и счётчики realtime-доставки. Повтор уже сохранённой той же ревизии обрабатывается идемпотентно.
Примечание
- входящий
type=3не только сохраняется как событие прочтения, но и обновляет серверный watermark диалога; - если подтверждение прочтения приходит не по порядку, сервер сохраняет наибольший watermark и пересчитывает
unreadCountпо фактическому состоянию сообщений; - это нужно, чтобы разные устройства не расходились по счётчику непрочитанных.
5. DeleteMessage
Принимает один signed DM-блок type=5 или type=6.
Запрос
{
"op": "DeleteMessage",
"requestId": "dm-del-001",
"payload": {
"blobB64": "BASE64_DELETE_BLOCK"
}
}
6. DeleteConversation
Принимает один signed DM-блок type=7 или type=8.
Запрос
{
"op": "DeleteConversation",
"requestId": "dm-del-all-001",
"payload": {
"blobB64": "BASE64_DELETE_CONVERSATION_BLOCK"
}
}
7. GetDirectMessages
Требует авторизации. Возвращает историю диалога с конкретным собеседником страницами.
Важно:
- начиная с 22 июля 2026 года сервер больше не высылает старую DM-историю автоматически при логине;
- после подключения клиент должен сам запросить первую страницу диалога;
- новые realtime-сообщения по-прежнему приходят событием
SignedMessageArrived. - в
GetDirectMessagesсервер отдаёт только обычные chat-сообщения (type=1/2), без read-receipt и delete/tombstone.
Запрос
{
"op": "GetDirectMessages",
"requestId": "dm-history-001",
"payload": {
"peerLogin": "bob",
"limit": 50,
"beforeTimeMs": 1774700000123,
"beforeMessageKey": "alice|bob|1774700000123|123456789|1"
}
}
beforeTimeMs и beforeMessageKey необязательны. Если их нет, сервер вернёт самую новую страницу.
Успешный ответ
{
"op": "GetDirectMessages",
"requestId": "dm-history-001",
"status": 200,
"ok": true,
"payload": {
"login": "alice",
"peerLogin": "bob",
"limit": 50,
"hasMore": true,
"nextBeforeTimeMs": 1774699999000,
"nextBeforeMessageKey": "alice|bob|1774699999000|123456780|2",
"messages": [
{
"messageKey": "alice|bob|1774700000123|123456789|1",
"baseKey": "alice|bob|1774700000123|123456789",
"fromLogin": "alice",
"toLogin": "bob",
"messageType": 1,
"timeMs": 1774700000123,
"nonce": 123456789,
"revisionTimeMs": 0,
"reencryptedAtMs": 0,
"createdAtMs": 1774700001123,
"readAtMs": 1774700001456,
"deliveryState": "delivered",
"blobB64": "BASE64_SIGNED_BLOCK"
}
]
}
}
Для следующей страницы клиент должен передать nextBeforeTimeMs и nextBeforeMessageKey из предыдущего ответа.
Поле deliveryState заполняется для исходящих элементов type=2. У входящих type=1 оно отсутствует/равно null.
8. Межсерверные операции DM
До отдельного этапа server-auth поля sourceServerLogin доверяются. Каждый SHiNE_DM всё равно заново проходит проверку формата и пользовательской подписи.
8.1. ReceiveOutcomingMessage
Прежняя операция передачи полной пары второму access-серверу отправителя. Delivery-state в межсерверный запрос не входит.
{
"op": "ReceiveOutcomingMessage",
"requestId": "dm-peer-001",
"payload": {
"incomingBlobB64": "BASE64_INCOMING",
"outgoingBlobB64": "BASE64_OUTGOING",
"sourceServerLogin": "server-a"
}
}
Ответ имеет прежний формат SendMessagePair. Принимающий сервер сохраняет пару идемпотентно и самостоятельно ставит локальную задачу доставки.
8.2. ReceiveIncomingMessage
Прежняя операция передачи одной входящей копии серверу получателя или второму серверу самого получателя.
{
"op": "ReceiveIncomingMessage",
"requestId": "dm-incoming-001",
"payload": {
"incomingBlobB64": "BASE64_INCOMING",
"sourceServerLogin": "server-a"
}
}
Успешный 2xx-ответ означает, что signed-блок проверен и находится в БД (новая или идемпотентно повторённая запись).
8.3. DmSyncBatch
Pull-синхронизация событий владельца с synced=false. ackSyncIds подтверждает версии событий, успешно сохранённые из предыдущего ответа.
{
"op": "DmSyncBatch",
"requestId": "dm-sync-001",
"payload": {
"ownerLogin": "alice",
"afterStoredAtMs": 0,
"afterMessageKey": "",
"limit": 500,
"maxBytes": 3000000,
"ackSyncIds": []
}
}
Ответ возвращает страницу несинхронизированных событий и курсор внутри текущего цикла:
{
"nextStoredAtMs": 1774700001123,
"nextMessageKey": "alice|bob|1774700000123|123456789|2",
"hasMore": false,
"items": [
{
"syncId": "alice|bob|1774700000123|123456789|2:0:0",
"primaryMessageKey": "alice|bob|1774700000123|123456789|2",
"storedAtMs": 1774700001123,
"blobsB64": ["BASE64_INCOMING", "BASE64_OUTGOING"]
}
]
}
Для пары порядок всегда incoming/outgoing; для входящей копии и tombstone массив содержит один blob. syncId — технический идентификатор конкретной ревизии только для ACK синхронизации, а не второй ID сообщения. После сохранения вызывающий сервер передаёт syncId в ackSyncIds следующего запроса. Только тогда источник ставит synced=true. Новый цикл начинается с afterStoredAtMs=0; полный сброс флагов поэтому повторно отдаёт всю историю.
8.4. GetDmDeliveryStatus
Единственная новая межсерверная операция доставки. Read-only проверка существующего messageKey; не изменяет состояние отвечающего сервера.
{
"op": "GetDmDeliveryStatus",
"requestId": "dm-status-001",
"payload": {
"messageKey": "alice|bob|1774700000123|123456789|2"
}
}
Ответ содержит messageKey, known и delivered. delivered=true означает доставку хотя бы одному серверу получателя.
Периодический процесс использует один логический последовательный сеанс поверх
постоянного WSS-пула: сначала синхронизирует настройки, затем вызывает
DmSyncBatch до завершения страниц и ACK. Существующий
MarkAllUserSettingsUnsynced также сбрасывает DM-флаги и возвращает
dmUpdated; отдельной операции MarkAllDmUnsynced нет.
Основные ошибки межсерверных операций:
400 / EMPTY_OWNER_LOGIN,EMPTY_MESSAGE_KEY;403 / LOCAL_SERVER_NOT_ACCESS_SERVER;500 / LOCAL_SERVER_NOT_CONFIGURED.
9. AckSessionDelivery
Требует авторизации. Подтверждает доставку в текущую сессию.
Запрос
{
"op": "AckSessionDelivery",
"requestId": "ack-001",
"payload": {
"messageKey": "from|to|time|nonce|1"
}
}
10. Событие SignedMessageArrived
Сервер присылает его по WebSocket в активные сессии адресата.
Payload события
{
"messageKey": "from|to|time|nonce|1",
"baseKey": "from|to|time|nonce",
"fromLogin": "alice",
"toLogin": "bob",
"targetLogin": "bob",
"messageType": 1,
"timeMs": 1774700000123,
"nonce": 123456789,
"blobB64": "BASE64_SIGNED_BLOCK",
"backlog": false
}
Если это новая ревизия того же письма, messageKey остаётся тем же, а revisionTimeMs меняется внутри бинарного блока.
Для типов 5/6/7/8 событие тоже приходит в таком же конверте, но логика применения определяется messageType и бинарным blobB64.
10.1. Событие DmDeliveryStateChanged
Сервер отправляет событие активным сессиям отправителя при изменении сетевого состояния исходящей пары.
{
"op": "DmDeliveryStateChanged",
"event": true,
"status": 200,
"payload": {
"baseKey": "alice|bob|1774700000123|123456789",
"outgoingKey": "alice|bob|1774700000123|123456789|2",
"deliveryState": "delivered"
}
}
failed приходит после последней неудачной попытки через час. Событие прочтения остаётся отдельным существующим read-receipt.
11. CallInviteBroadcast
Требует авторизации. Шлёт приглашение к звонку в активные сессии toLogin.
12. CallSignalToSession
Требует авторизации. Шлёт сигнал звонка в конкретную сессию.
13. Замечания
- все DM-типы
1..8используютSHiNE_DM GetUserможет lazy-import пользователя из Solana PDA, поэтому именно через него клиент обычно получаетclientKeyадресата для E2EE- сервер не расшифровывает DM; в списке диалогов он отдаёт последний signed block как
lastMessageBlobB64, а не извлекает plaintext preview - сервер хранит последнюю применённую версию контентного сообщения по правилу
revisionTimeMs, а при равенстве — поreencryptedAtMs - если сервер уже знает tombstone удаления переписки и получает старое сообщение до этой границы, он перерассылает известный
DeleteConversationнаaccess_serversобеих сторон - HTTP endpoints для DM-файлов сейчас отсутствуют