Merge branch 'main' into codex/analyze-block-addition-functionality-and-create-api-docs

This commit is contained in:
ai5590 2026-03-30 00:36:29 +03:00 committed by GitHub
commit 1c9841b4a6
10 changed files with 260 additions and 6 deletions

View File

@ -20,6 +20,10 @@
0. **API/04_Add_Block_to_Blockchain_API.md**
Глава API по записи блоков: формат `AddBlock`, коды ошибок, поддержанные типы/подтипы блоков и рекомендации по ресинхронизации.
1. **01_Connection_and_Sessions.md**
0. **API/05_Technical_Requests_API.md**
Технические запросы без авторизации: `Ping` для keep-alive и `GetServerInfo` для проверки доступности узла и чтения его публичной информации.
1. **01_Connection_and_Sessions.md**
Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий.

View File

@ -0,0 +1,137 @@
# API для разработчиков: Технические запросы
Этот файл описывает технические запросы, которые не требуют авторизации и нужны для служебной работы клиента с сервером.
Сейчас здесь два метода:
- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения;
- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети.
Логика раздела такая:
- `Ping` нужен для регулярной проверки, что соединение всё ещё живо;
- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла.
Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов.
## 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. Короткое резюме
- `Ping` нужен для keep-alive и проверки, что WebSocket-соединение живо.
- `GetServerInfo` нужен для выбора сервера в сети и показа публичной информации об узле.
- Оба запроса доступны без авторизации.

View File

@ -5,12 +5,19 @@ plugins {
}
group = 'shine'
version = '1.0'
version = '1.1_codex'
tasks.jar {
enabled = false // это что бы не создавала обычный джар, а будет только толстый джар
}
tasks.processResources {
filteringCharset = 'UTF-8'
filesMatching('application.properties') {
expand(projectVersion: project.version.toString())
}
}
repositories {
mavenCentral()
}
@ -142,4 +149,3 @@ tasks.register('itDeployServer', JavaExec) {
dependsOn testClasses
}

View File

@ -45,6 +45,12 @@ public final class AppConfig {
return properties.getProperty(name);
}
/** Вернёт строку или пустую строку, если параметр не найден. */
public String getStringOrEmpty(String name) {
String value = properties.getProperty(name);
return value == null ? "" : value.trim();
}
/** Можно добавить методы для удобства */
public int getInt(String name, int defaultValue) {
String v = properties.getProperty(name);

View File

@ -47,7 +47,9 @@ import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Ha
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
// --- NEW: Ping ---
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
import java.util.Map;
@ -85,7 +87,8 @@ public final class JsonHandlerRegistry {
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
// --- system ---
Map.entry("Ping", new Net_Ping_Handler())
Map.entry("Ping", new Net_Ping_Handler()),
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler())
// --- subscriptions ---
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
@ -118,7 +121,8 @@ public final class JsonHandlerRegistry {
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
// --- system ---
Map.entry("Ping", Net_Ping_Request.class)
Map.entry("Ping", Net_Ping_Request.class),
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class)
);
private JsonHandlerRegistry() { }

View File

@ -1,3 +1,14 @@
server.1port=7070
db.path=data/shine.sqlite
# ------------------------------------------------------------
# Server public info
# Эти поля используются JSON-операцией GetServerInfo.
# Если какое-то значение не задано, сервер вернёт пустую строку.
# ------------------------------------------------------------
server.version=${projectVersion}
server.info.url=
server.info.physicalRegion=
server.info.description=
server.info.origin=
server.info.extraInfo=

View File

@ -36,6 +36,8 @@ public class IT_01_AddUser {
try (WsSession ws = WsSession.open()) {
checkPingAndServerInfo(r, ws, t);
r.ok("AddUser USER1: " + TestConfig.LOGIN());
String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t);
checkAddUser200or409(r, resp1);
@ -76,6 +78,38 @@ public class IT_01_AddUser {
return r.summaryLine();
}
private static void checkPingAndServerInfo(TestResult r, WsSession ws, Duration t) {
String pingResp = ws.call("Ping", JsonBuilders.ping(System.currentTimeMillis()), t);
if (JsonParsers.status(pingResp) != 200 || !Boolean.TRUE.equals(JsonParsers.ok(pingResp))) {
r.fail("Ping: ожидали status=200 и ok=true, resp=" + pingResp);
fail("Ping unexpected response");
}
Long serverTs = JsonParsers.pingTs(pingResp);
if (serverTs == null || serverTs <= 0) {
r.fail("Ping: сервер не вернул ts, resp=" + pingResp);
fail("Ping missing ts");
}
r.ok("Ping: ok, serverTs=" + serverTs);
String infoResp = ws.call("GetServerInfo", JsonBuilders.getServerInfo(), t);
if (JsonParsers.status(infoResp) != 200 || !Boolean.TRUE.equals(JsonParsers.ok(infoResp))) {
r.fail("GetServerInfo: ожидали status=200 и ok=true, resp=" + infoResp);
fail("GetServerInfo unexpected response");
}
if (!JsonParsers.payloadIsObject(infoResp)) {
r.fail("GetServerInfo: payload должен быть объектом, resp=" + infoResp);
fail("GetServerInfo payload is not object");
}
r.ok("GetServerInfo: ok, url='" + safe(JsonParsers.payloadText(infoResp, "url"))
+ "', version='" + safe(JsonParsers.payloadText(infoResp, "version"))
+ "', physicalRegion='" + safe(JsonParsers.payloadText(infoResp, "physicalRegion"))
+ "', description='" + safe(JsonParsers.payloadText(infoResp, "description"))
+ "', origin='" + safe(JsonParsers.payloadText(infoResp, "origin"))
+ "', extraInfo='" + safe(JsonParsers.payloadText(infoResp, "extraInfo")) + "'");
}
private static void checkAddUser200or409(TestResult r, String resp) {
int st = JsonParsers.status(resp);
if (st == 200) {
@ -324,4 +358,8 @@ public class IT_01_AddUser {
private static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
private static String safe(String s) {
return s == null ? "" : s;
}
}

View File

@ -1,6 +1,7 @@
package test.it.runner;
import test.it.cases.IT_01_AddUser;
import test.it.cases.IT_00_TechnicalRequests;
import test.it.cases.IT_02_Sessions;
import test.it.cases.IT_03_AddBlock_NoAuth;
import test.it.cases.IT_04_UserParams_NoAuth;
@ -37,6 +38,9 @@ public class IT_RunAllMain {
TestLog.title("IT RUN: запуск всех тестов подряд"
+ (STOP_ON_FIRST_FAIL ? " (STOP_ON_FIRST_FAIL=ON)" : " (STOP_ON_FIRST_FAIL=OFF)"));
String s0 = IT_00_TechnicalRequests.run(); summaries.add(s0);
if (s0.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
String s1 = IT_01_AddUser.run(); summaries.add(s1);
if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }

View File

@ -90,6 +90,35 @@ public final class JsonBuilders {
""".formatted(requestId, login);
}
// ---------------- Ping ----------------
public static String ping(long ts) {
String requestId = TestIds.next("ping");
return """
{
"op": "Ping",
"requestId": "%s",
"payload": {
"ts": %d
}
}
""".formatted(requestId, ts);
}
// ---------------- GetServerInfo ----------------
public static String getServerInfo() {
String requestId = TestIds.next("serverinfo");
return """
{
"op": "GetServerInfo",
"requestId": "%s",
"payload": {
}
}
""".formatted(requestId);
}
// ---------------- AuthChallenge ----------------
public static String authChallenge(String login) {

View File

@ -132,6 +132,21 @@ public final class JsonParsers {
}
}
public static Long pingTs(String json) {
try {
JsonNode root = MAPPER.readTree(json);
JsonNode payload = root.get("payload");
if (payload != null && payload.has("ts")) return payload.get("ts").asLong();
return null;
} catch (Exception e) {
return null;
}
}
public static String payloadText(String json, String field) {
return getPayloadText(json, field);
}
public static List<String> sessionIds(String json) {
List<String> res = new ArrayList<>();
try {