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

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

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

- `Dev_Docs/Personal_Messages/README.md`

## 1. `UpsertPushToken`

Требует авторизации.

### Запрос

```json
{
  "op": "UpsertPushToken",
  "requestId": "push-upsert-001",
  "payload": {
    "sessionId": "SESSION_ID",
    "endpoint": "https://push.example/...",
    "p256dhKey": "BASE64",
    "authKey": "BASE64",
    "platform": "web",
    "userAgent": "Mozilla/5.0 ..."
  }
}
```

### Успешный ответ

```json
{
  "op": "UpsertPushToken",
  "requestId": "push-upsert-001",
  "status": 200,
  "ok": true,
  "payload": {
    "tokenId": "token-1",
    "updatedAtMs": 1774700000123
  }
}
```

## 2. `SendTestWebPush`

Требует авторизации.

### Запрос

```json
{
  "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`

Для контентных сообщений `type=1/2` внутри base64 лежит бинарный формат `SHiNE_DM`.

### Запрос

```json
{
  "op": "SendMessagePair",
  "requestId": "dm-pair-001",
  "payload": {
    "incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK",
    "outgoingBlobB64": "BASE64_OUTGOING_SIGNED_BLOCK"
  }
}
```

### Успешный ответ

```json
{
  "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",
    "deliveredWsSessions": 1,
    "deliveredWebPushSessions": 0
  }
}
```

### Ошибки

- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
- `400 / BAD_CONTENT_FORMAT` — для контентного сообщения пришёл не `SHiNE_DM`
- `400 / ATTACHMENTS_DISABLED` — в `SHiNE_DM` пришёл `attachmentsCount != 0`
- `404 / USER_NOT_FOUND` — один из логинов не найден
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку

## 4. `ReceiveIncomingMessage`

Принимает только один входящий signed DM-блок.

### Назначение

Используется там, где нужно принять только incoming-вариант сообщения.

### Запрос

```json
{
  "op": "ReceiveIncomingMessage",
  "requestId": "dm-in-001",
  "payload": {
    "incomingBlobB64": "BASE64_INCOMING_SIGNED_BLOCK"
  }
}
```

## 5. `AckSessionDelivery`

Требует авторизации. Подтверждает доставку в текущую сессию.

### Запрос

```json
{
  "op": "AckSessionDelivery",
  "requestId": "ack-001",
  "payload": {
    "messageKey": "from|to|time|nonce|1"
  }
}
```

## 6. Событие `SignedMessageArrived`

Сервер присылает его по WebSocket в активные сессии адресата.

### Payload события

```json
{
  "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` меняется внутри бинарного блока.

## 7. `CallInviteBroadcast`

Требует авторизации. Шлёт приглашение к звонку в активные сессии `toLogin`.

## 8. `CallSignalToSession`

Требует авторизации. Шлёт сигнал звонка в конкретную сессию.

## 9. Замечания

- read-receipt `type=3/4` пока остаются в legacy-формате `SHiNE_dm2`
- контентные DM `type=1/2` используют `SHiNE_DM`
- сервер хранит только последнюю версию контентного сообщения по `messageKey`
- удаление сообщения реализуется новой ревизией с пустым телом и `attachmentsCount = 0`
- HTTP endpoints для DM-файлов сейчас отсутствуют
