Files
SHiNE-server/docs/API/12_Direct_Messages_Push_Calls_API.md
T

17 KiB
Raw Blame History

API для разработчиков: DM, push и сигналы звонков

Документ описывает публичные операции, связанные с личными сообщениями, WebPush и сигналами звонков.

Подробная логика DM и бинарного формата:

  • docs/Personal_Messages/Протокол_DM_v1.md
  • docs/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=3
  • outgoingBlobB64 — блок 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 или outgoingBlobB64
  • 400 / BAD_BLOCK_FORMAT — base64 или бинарный контейнер повреждён
  • 400 / BAD_CRYPTO_METHOD — неподдерживаемый метод шифрования контейнера EncryptedBody_v1_0
  • 404 / USER_NOT_FOUND — один из логинов не найден даже после попытки lazy-import из Solana PDA
  • 460 / 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-файлов сейчас отсутствуют