616 lines
22 KiB
Markdown
616 lines
22 KiB
Markdown
# API для разработчиков: Технические запросы
|
||
|
||
Этот файл описывает технические WebSocket-запросы, которые нужны для служебной работы клиента с сервером. Часть операций доступна без авторизации, часть требует успешной авторизованной сессии.
|
||
|
||
Сейчас здесь девять методов:
|
||
|
||
- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения;
|
||
- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети;
|
||
- `ListBlockchainHeads` — краткая сводка по всем локальным блокчейнам сервера для межсерверной синхронизации;
|
||
- `GetSyncUserProfile` — межсерверный профиль пользователя для создания локальной цепочки без Solana RPC;
|
||
- `SendSignal` — общий межсессионный технический сигнал в одну конкретную сессию или сразу во все активные сессии пользователя;
|
||
- `GetCallIceConfig` — выдача STUN/TURN конфигурации для звонков;
|
||
- `ClientErrorLog` — отправка клиентской ошибки в серверный лог;
|
||
- `ClientDebugLog` — отправка клиентского debug-события в серверный буфер;
|
||
- `CallDeliveryReport` — диагностический отчёт клиента о доставке/установке звонка.
|
||
|
||
Логика раздела такая:
|
||
|
||
- `Ping` нужен для регулярной проверки, что соединение всё ещё живо;
|
||
- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла.
|
||
- `ListBlockchainHeads` нужен для сервер-сервер сверки: партнёр получает список heads по всем цепочкам, сравнивает его со своим состоянием и затем добирает недостающие блоки по диапазону.
|
||
- `GetSyncUserProfile` нужен для server-to-server режима, когда принимающий сервер хочет создать у себя локальные `solana_users + blockchain_state` без прямого обращения в Solana. Это используется как временный обход ограничений внешнего Solana RPC.
|
||
- `SendSignal` нужен для доверенных межсессионных команд одного пользователя. Первое практическое применение — `remote AddBlock via homeserver session`, но формат задуман как общий transport на вырост.
|
||
|
||
Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов.
|
||
|
||
## 1. `Ping`
|
||
|
||
### Назначение
|
||
|
||
Служебный keep-alive запрос.
|
||
|
||
Клиент может отправлять его периодически, чтобы:
|
||
|
||
- поддерживать активное WebSocket-соединение;
|
||
- понимать, что сервер отвечает;
|
||
- при необходимости получать текущее серверное время.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "Ping",
|
||
"requestId": "ping-001",
|
||
"payload": {
|
||
"ts": 1774700000123
|
||
}
|
||
}
|
||
```
|
||
|
||
Поле `ts` в запросе необязательно для логики сервера. Сервер его не валидирует и не использует для принятия решения.
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "Ping",
|
||
"requestId": "ping-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"ts": 1774700000456
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `Ping`
|
||
|
||
- У `Ping` нет специальных прикладных ошибок.
|
||
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||
|
||
---
|
||
|
||
## 2. `GetServerInfo`
|
||
|
||
### Назначение
|
||
|
||
Запрос публичной информации о сервере.
|
||
|
||
Он нужен клиенту для выбора сервера в децентрализованной сети. По этому запросу клиент может:
|
||
|
||
- проверить, что сервер вообще доступен;
|
||
- показать URL и версию сервера;
|
||
- показать физический регион или адрес размещения;
|
||
- показать описание сервера;
|
||
- показать поле `origin` как комментарий о природе этого узла;
|
||
- показать дополнительную текстовую информацию.
|
||
|
||
Этот запрос доступен без авторизации.
|
||
|
||
### Источник данных
|
||
|
||
- `version` берётся из Gradle build и подставляется в `application.properties`;
|
||
- остальные поля читаются из настроек сервера;
|
||
- если значение в конфиге не задано, сервер возвращает пустую строку.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "GetServerInfo",
|
||
"requestId": "srv-001",
|
||
"payload": {
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "GetServerInfo",
|
||
"requestId": "srv-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"url": "wss://node.example.org/ws",
|
||
"version": "1.0",
|
||
"physicalRegion": "Грузия, Тбилиси",
|
||
"description": "Public community SHiNE node",
|
||
"origin": "Community-operated node",
|
||
"extraInfo": "IPv4 + IPv6; test federation enabled"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Поля ответа
|
||
|
||
- `url` — публичный URL сервера.
|
||
- `version` — версия сервера из Gradle build.
|
||
- `physicalRegion` — физический регион или адрес размещения сервера.
|
||
- `description` — человекочитаемое описание сервера.
|
||
- `origin` — комментарий о том, какой это сервер.
|
||
- `extraInfo` — любая дополнительная информация о сервере.
|
||
|
||
### Специфические коды ошибок `GetServerInfo`
|
||
|
||
- У `GetServerInfo` нет специальных прикладных ошибок при штатной работе.
|
||
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||
|
||
---
|
||
|
||
## 3. `ListBlockchainHeads`
|
||
|
||
### Назначение
|
||
|
||
Запрос краткой сводки по всем локальным блокчейнам сервера.
|
||
|
||
Нужен для межсерверной синхронизации. Партнёр может:
|
||
|
||
- получить список всех блокчейнов;
|
||
- сравнить `lastBlockNumber` и `lastBlockHash` со своими значениями;
|
||
- понять, какие цепочки нужно догонять;
|
||
- затем отдельно запросить недостающие блоки по диапазону.
|
||
|
||
Этот запрос доступен без авторизации.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "ListBlockchainHeads",
|
||
"requestId": "heads-001",
|
||
"payload": {}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "ListBlockchainHeads",
|
||
"requestId": "heads-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"blockchains": [
|
||
{
|
||
"blockchainName": "alice_main",
|
||
"lastBlockNumber": 124,
|
||
"lastBlockHash": "aabbccdd00112233445566778899aabbccddeeff00112233445566778899aabb",
|
||
"fileSizeBytes": 58720
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### Поля ответа
|
||
|
||
- `blockchains` — массив текущих heads всех цепочек сервера.
|
||
- `blockchainName` — имя блокчейна.
|
||
- `lastBlockNumber` — последний номер блока в этой цепочке.
|
||
- `lastBlockHash` — последний хэш блока в HEX-формате `64` символа.
|
||
- `fileSizeBytes` — текущий размер файла блокчейна в байтах.
|
||
|
||
### Специфические коды ошибок `ListBlockchainHeads`
|
||
|
||
- У `ListBlockchainHeads` нет специальных прикладных ошибок при штатной работе.
|
||
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||
|
||
---
|
||
|
||
## 4. `GetSyncUserProfile`
|
||
|
||
### Назначение
|
||
|
||
Запрос минимального профиля пользователя для межсерверной синхронизации.
|
||
|
||
Нужен в сценарии, когда сервер во время periodic sync увидел чужой блокчейн, которого у него локально ещё нет. Вместо обращения в Solana PDA он может запросить у партнёра:
|
||
|
||
- `login`
|
||
- `blockchainName`
|
||
- `solanaKey`
|
||
- `blockchainKey`
|
||
- `clientKey`
|
||
- `blockchainSizeLimitBytes`
|
||
|
||
После этого принимающий сервер может локально создать записи в `solana_users` и `blockchain_state`, а затем уже докачивать блоки через `GetBlockchainBlock`.
|
||
|
||
Этот запрос доступен без авторизации и предназначен именно для server-to-server sync.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "GetSyncUserProfile",
|
||
"requestId": "sync-user-001",
|
||
"payload": {
|
||
"login": "alice"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ: пользователь не найден
|
||
|
||
```json
|
||
{
|
||
"op": "GetSyncUserProfile",
|
||
"requestId": "sync-user-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"exists": false
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ: пользователь найден
|
||
|
||
```json
|
||
{
|
||
"op": "GetSyncUserProfile",
|
||
"requestId": "sync-user-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"exists": true,
|
||
"login": "alice",
|
||
"blockchainName": "alice-001",
|
||
"solanaKey": "BASE64_32",
|
||
"blockchainKey": "BASE64_32",
|
||
"clientKey": "BASE64_32",
|
||
"blockchainSizeLimitBytes": 100000
|
||
}
|
||
}
|
||
```
|
||
|
||
### Поля ответа
|
||
|
||
- `exists` — найден ли пользователь на сервере-партнёре.
|
||
- `login` — канонический login из БД сервера-партнёра.
|
||
- `blockchainName` — имя основной цепочки пользователя.
|
||
- `solanaKey` — публичный ключ логина.
|
||
- `blockchainKey` — публичный ключ блокчейна.
|
||
- `clientKey` — публичный клиентский ключ, который в текущей модели используется при создании локальной записи.
|
||
- `blockchainSizeLimitBytes` — лимит размера файла блокчейна, который будет записан в локальный `blockchain_state`.
|
||
|
||
### Специфические коды ошибок `GetSyncUserProfile`
|
||
|
||
- `400 / BAD_FIELDS` — пустой или некорректный `login`.
|
||
- `404 / BLOCKCHAIN_STATE_NOT_FOUND` — пользователь найден, но на сервере-партнёре отсутствует `blockchain_state` для его цепочки.
|
||
- При непредвиденной ошибке сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||
|
||
---
|
||
|
||
## 5. `SendSignal`
|
||
|
||
Доступно только после успешной авторизации.
|
||
|
||
### Назначение
|
||
|
||
Общий межсессионный технический сигнал.
|
||
|
||
Этот метод нужен для случаев, когда одна активная сессия пользователя должна быстро передать служебную команду другой сессии того же пользователя или сразу всем его активным сессиям.
|
||
|
||
Первый целевой сценарий:
|
||
|
||
- `remote AddBlock via homeserver session`
|
||
|
||
То есть телефон без локального `blockchain.key` может:
|
||
|
||
- подготовить только сырой payload операции без текущей вершины цепочки;
|
||
- подписать сам `SendSignal` своим `session key`;
|
||
- дополнительно подписать его `client key`, чтобы homeserver/ESP32 точно видел, что запрос пришёл от доверенного клиента этого же логина;
|
||
- отправить запрос в выбранную `homeserver`-сессию;
|
||
- получить от неё ответ после настоящего `AddBlock`, который homeserver соберёт и подпишет уже сама.
|
||
|
||
### Режимы доставки
|
||
|
||
- `targetMode = "single_session"` — доставка в одну конкретную `targetSessionId`.
|
||
- `targetMode = "all_sessions"` — доставка во все активные сессии указанного логина.
|
||
|
||
### Важное правило подписи
|
||
|
||
Сам `SendSignal` не подписывает поле `data` отдельной вложенной подписью. Вместо этого сервер проверяет подписи по общему preimage сигнала, в который входит:
|
||
|
||
- `fromLogin`
|
||
- `fromSessionId`
|
||
- `toLogin`
|
||
- `targetMode`
|
||
- `targetSessionId`
|
||
- `signalType`
|
||
- `signalRequestId`
|
||
- `timeMs`
|
||
- `sha256(data)`
|
||
|
||
Поддерживаются две подписи:
|
||
|
||
- `sessionSignatureB64` — обязательная подпись текущей авторизованной `session key`;
|
||
- `clientSignatureB64` — необязательная подпись `client key`.
|
||
|
||
Для сценария `remote AddBlock via homeserver` текущая договорённость такая:
|
||
|
||
- запрос должен идти только своему же логину;
|
||
- запрос должен быть подписан и `session key`, и `client key`;
|
||
- в будущем для отдельных wallet-сценариев `clientSignatureB64` может быть пустой.
|
||
|
||
### Запрос в одну сессию
|
||
|
||
```json
|
||
{
|
||
"op": "SendSignal",
|
||
"requestId": "ws-req-001",
|
||
"payload": {
|
||
"toLogin": "alice",
|
||
"targetMode": "single_session",
|
||
"targetSessionId": "sess-hs-001",
|
||
"signalType": "remote_addblock_request",
|
||
"signalRequestId": "remote-addblock-001",
|
||
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
|
||
"timeMs": 1774700000123,
|
||
"sessionSignatureB64": "BASE64_64",
|
||
"clientSignatureB64": "BASE64_64"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "SendSignal",
|
||
"requestId": "ws-req-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"deliveredCount": 1,
|
||
"deliveredSessionIds": ["sess-hs-001"]
|
||
}
|
||
}
|
||
```
|
||
|
||
### Событие на принимающей стороне
|
||
|
||
```json
|
||
{
|
||
"op": "IncomingSignal",
|
||
"eventId": "evt-001",
|
||
"payload": {
|
||
"fromLogin": "alice",
|
||
"fromSessionId": "sess-phone-001",
|
||
"toLogin": "alice",
|
||
"targetMode": "single_session",
|
||
"targetSessionId": "sess-hs-001",
|
||
"signalType": "remote_addblock_request",
|
||
"signalRequestId": "remote-addblock-001",
|
||
"data": "{\"operation\":\"remote_addblock_request\",\"signalRequestId\":\"remote-addblock-001\",\"blockchainName\":\"alice_main\",\"blockBodyB64\":\"...\"}",
|
||
"timeMs": 1774700000123,
|
||
"sessionSignatureB64": "BASE64_64",
|
||
"clientSignatureB64": "BASE64_64",
|
||
"dataSha256B64": "BASE64_32"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфика `remote AddBlock`
|
||
|
||
Для `remote_addblock_request` поле `data` теперь содержит:
|
||
|
||
- `blockchainName`
|
||
- `blockBodyB64`
|
||
|
||
Где `blockBodyB64` — это не финальный блок и не почти готовый preimage, а компактный бинарный контейнер:
|
||
|
||
- `msgType` (`u16`)
|
||
- `msgSubType` (`u16`)
|
||
- `msgVersion` (`u16`)
|
||
- `bodyBytes`
|
||
|
||
После этого homeserver сама:
|
||
|
||
- вызывает `GetUser(login)` и получает `serverLastGlobalNumber/serverLastGlobalHash`;
|
||
- вычисляет новый `blockNumber = last + 1`;
|
||
- подставляет актуальный `prevBlockHash`;
|
||
- ставит текущее время;
|
||
- досчитывает полный preimage;
|
||
- подписывает его своим `blockchain key`;
|
||
- и только потом делает настоящий `AddBlock`.
|
||
|
||
### Специфические коды ошибок `SendSignal`
|
||
|
||
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
|
||
- `400 / BAD_FIELDS` — не хватает обязательных полей или нарушено правило `single_session/all_sessions`.
|
||
- `400 / BAD_TARGET_MODE` — передан неизвестный `targetMode`.
|
||
- `400 / TIME_SKEW` — `timeMs` отличается от серверного более чем на 30 секунд.
|
||
- `500 / NO_CLIENT_KEY` — для текущего пользователя не найден `client key`.
|
||
- `404 / USER_NOT_FOUND` — логин адресата не найден.
|
||
- `400 / BAD_DATA` — сервер не смог обработать `data`.
|
||
- `400 / BAD_SESSION_SIGNATURE` — некорректная подпись `session key`.
|
||
- `400 / BAD_CLIENT_SIGNATURE` — некорректная подпись `client key`.
|
||
- `404 / SESSION_NOT_FOUND` — при `single_session` целевая сессия не найдена или не онлайн.
|
||
- `404 / NO_TARGET_SESSIONS` — при `all_sessions` у пользователя сейчас нет активных онлайн-сессий.
|
||
- `404 / DELIVERY_FAILED` — сервер не смог отправить событие ни в одну из целевых сессий.
|
||
|
||
---
|
||
|
||
## 6. `GetCallIceConfig`
|
||
|
||
Доступно только после успешной авторизации.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "GetCallIceConfig",
|
||
"requestId": "ice-001",
|
||
"payload": {
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "GetCallIceConfig",
|
||
"requestId": "ice-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"stunUrls": ["stun:stun.example.org:3478"],
|
||
"turnUrls": ["turn:turn.example.org:3478?transport=udp"],
|
||
"turnUsername": "user",
|
||
"turnPassword": "password",
|
||
"turnServers": [
|
||
{
|
||
"id": "primary",
|
||
"urls": ["turn:turn.example.org:3478?transport=udp"],
|
||
"username": "user",
|
||
"password": "password"
|
||
}
|
||
],
|
||
"turnEnabled": true,
|
||
"generatedAtMs": 1774700000123,
|
||
"expiresAtMs": 1774700300123,
|
||
"ttlSec": 300
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `GetCallIceConfig`
|
||
|
||
- `422 / NOT_AUTHENTICATED` — требуется авторизация.
|
||
|
||
---
|
||
|
||
## 7. `ClientErrorLog`
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "ClientErrorLog",
|
||
"requestId": "err-001",
|
||
"payload": {
|
||
"kind": "global_error",
|
||
"message": "TypeError: failed",
|
||
"stack": "...",
|
||
"sourceUrl": "https://shineup.me/app.js",
|
||
"lineNumber": 10,
|
||
"columnNumber": 20,
|
||
"route": "#/channel-view/own-0",
|
||
"href": "https://shineup.me/#/channel-view/own-0",
|
||
"userAgent": "...",
|
||
"clientTs": 1774700000123,
|
||
"requestOp": "GetChannelMessages",
|
||
"requestIdRef": "GetChannelMessages-123",
|
||
"contextJson": "{\"screen\":\"channels\"}"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "ClientErrorLog",
|
||
"requestId": "err-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"serverTs": 1774700000456,
|
||
"accepted": true
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `ClientErrorLog`
|
||
|
||
- `400 / BAD_FIELDS` — обязательные поля ошибки не заполнены.
|
||
|
||
---
|
||
|
||
## 8. `ClientDebugLog`
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "ClientDebugLog",
|
||
"requestId": "dbg-001",
|
||
"payload": {
|
||
"runId": "ui-run-1",
|
||
"level": "info",
|
||
"message": "opened channels tab",
|
||
"details": "{\"route\":\"#/channels\"}"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "ClientDebugLog",
|
||
"requestId": "dbg-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"accepted": true,
|
||
"serverTs": 1774700000456
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `ClientDebugLog`
|
||
|
||
- `400 / BAD_FIELDS` — поле `message` не заполнено.
|
||
|
||
---
|
||
|
||
## 9. `CallDeliveryReport`
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "CallDeliveryReport",
|
||
"requestId": "call-report-001",
|
||
"payload": {
|
||
"type": "outgoing_failed",
|
||
"value": "{\"reason\":\"ice_failed\",\"callId\":\"call-1\"}"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "CallDeliveryReport",
|
||
"requestId": "call-report-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"serverTs": 1774700000456,
|
||
"accepted": true
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `CallDeliveryReport`
|
||
|
||
- `400 / BAD_FIELDS` — поле `type` не заполнено.
|
||
|
||
---
|
||
|
||
## 10. Короткое резюме
|
||
|
||
- `Ping` нужен для keep-alive и проверки, что WebSocket-соединение живо.
|
||
- `GetServerInfo` нужен для выбора сервера в сети и показа публичной информации об узле.
|
||
- `SendSignal` нужен для доверенных межсессионных сигналов одного пользователя, включая `remote AddBlock via homeserver session`.
|
||
- `GetCallIceConfig` нужен для WebRTC-звонков и требует авторизации.
|
||
- `ClientErrorLog`, `ClientDebugLog`, `CallDeliveryReport` используются для диагностики клиента и звонков.
|