Добавил АПИ функцию которая возвращает информацию о версии сервера и о том что он работает
This commit is contained in:
AidarKC 2026-03-30 00:34:16 +03:00
parent 1aabcf4d80
commit 99cf000f24
10 changed files with 259 additions and 6 deletions

View File

@ -16,6 +16,9 @@
0. **API/03_Session_Management_API.md** 0. **API/03_Session_Management_API.md**
Глава API по управлению сессиями: `ListSessions` и `CloseActiveSession`. Глава API по управлению сессиями: `ListSessions` и `CloseActiveSession`.
0. **API/05_Technical_Requests_API.md**
Технические запросы без авторизации: `Ping` для keep-alive и `GetServerInfo` для проверки доступности узла и чтения его публичной информации.
1. **01_Connection_and_Sessions.md** 1. **01_Connection_and_Sessions.md**
Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий. Процесс подключения к 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' group = 'shine'
version = '1.0' version = '1.1_codex'
tasks.jar { tasks.jar {
enabled = false // это что бы не создавала обычный джар, а будет только толстый джар enabled = false // это что бы не создавала обычный джар, а будет только толстый джар
} }
tasks.processResources {
filteringCharset = 'UTF-8'
filesMatching('application.properties') {
expand(projectVersion: project.version.toString())
}
}
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -142,4 +149,3 @@ tasks.register('itDeployServer', JavaExec) {
dependsOn testClasses dependsOn testClasses
} }

View File

@ -45,6 +45,12 @@ public final class AppConfig {
return properties.getProperty(name); 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) { public int getInt(String name, int defaultValue) {
String v = properties.getProperty(name); 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; import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
// --- NEW: Ping --- // --- 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.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 server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
import java.util.Map; import java.util.Map;
@ -85,7 +87,8 @@ public final class JsonHandlerRegistry {
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()), Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
// --- system --- // --- system ---
Map.entry("Ping", new Net_Ping_Handler()) Map.entry("Ping", new Net_Ping_Handler()),
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler())
// --- subscriptions --- // --- subscriptions ---
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler()) // Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
@ -118,7 +121,8 @@ public final class JsonHandlerRegistry {
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class), Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
// --- system --- // --- system ---
Map.entry("Ping", Net_Ping_Request.class) Map.entry("Ping", Net_Ping_Request.class),
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class)
); );
private JsonHandlerRegistry() { } private JsonHandlerRegistry() { }
@ -130,4 +134,4 @@ public final class JsonHandlerRegistry {
public static Map<String, Class<? extends Net_Request>> getRequestTypes() { public static Map<String, Class<? extends Net_Request>> getRequestTypes() {
return REQUEST_TYPES; return REQUEST_TYPES;
} }
} }

View File

@ -1,3 +1,14 @@
server.1port=7070 server.1port=7070
db.path=data/shine.sqlite 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()) { try (WsSession ws = WsSession.open()) {
checkPingAndServerInfo(r, ws, t);
r.ok("AddUser USER1: " + TestConfig.LOGIN()); r.ok("AddUser USER1: " + TestConfig.LOGIN());
String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t); String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t);
checkAddUser200or409(r, resp1); checkAddUser200or409(r, resp1);
@ -76,6 +78,38 @@ public class IT_01_AddUser {
return r.summaryLine(); 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) { private static void checkAddUser200or409(TestResult r, String resp) {
int st = JsonParsers.status(resp); int st = JsonParsers.status(resp);
if (st == 200) { if (st == 200) {
@ -324,4 +358,8 @@ public class IT_01_AddUser {
private static boolean isBlank(String s) { private static boolean isBlank(String s) {
return s == null || s.trim().isEmpty(); 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; package test.it.runner;
import test.it.cases.IT_01_AddUser; 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_02_Sessions;
import test.it.cases.IT_03_AddBlock_NoAuth; import test.it.cases.IT_03_AddBlock_NoAuth;
import test.it.cases.IT_04_UserParams_NoAuth; import test.it.cases.IT_04_UserParams_NoAuth;
@ -37,6 +38,9 @@ public class IT_RunAllMain {
TestLog.title("IT RUN: запуск всех тестов подряд" TestLog.title("IT RUN: запуск всех тестов подряд"
+ (STOP_ON_FIRST_FAIL ? " (STOP_ON_FIRST_FAIL=ON)" : " (STOP_ON_FIRST_FAIL=OFF)")); + (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); String s1 = IT_01_AddUser.run(); summaries.add(s1);
if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); } if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
@ -69,4 +73,4 @@ public class IT_RunAllMain {
return failed; return failed;
} }
} }

View File

@ -90,6 +90,35 @@ public final class JsonBuilders {
""".formatted(requestId, login); """.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 ---------------- // ---------------- AuthChallenge ----------------
public static String authChallenge(String login) { 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) { public static List<String> sessionIds(String json) {
List<String> res = new ArrayList<>(); List<String> res = new ArrayList<>();
try { try {