diff --git a/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java b/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java new file mode 100644 index 0000000..3d03346 --- /dev/null +++ b/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java @@ -0,0 +1,128 @@ +package shine.db.dao; + +import shine.db.SqliteDbController; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * ConnectionsStateDAO — чтение текущего состояния связей из connections_state. + * + * ВАЖНО: + * - login в запросах может быть в любом регистре, поэтому в WHERE используем COLLATE NOCASE + * - в ответах возвращаем логины в каноническом регистре через JOIN на solana_users + * + * ПРИМЕЧАНИЕ: + * Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй в SQL. + */ +public final class ConnectionsStateDAO { + + private static volatile ConnectionsStateDAO instance; + private final SqliteDbController db = SqliteDbController.getInstance(); + + private ConnectionsStateDAO() {} + + public static ConnectionsStateDAO getInstance() { + if (instance == null) { + synchronized (ConnectionsStateDAO.class) { + if (instance == null) instance = new ConnectionsStateDAO(); + } + } + return instance; + } + + /** + * Outgoing: список логинов (канонических), кому login поставил relType. + */ + public List listOutgoingByRelTypeCanonical(Connection c, String loginAnyCase, int relType) throws SQLException { + String sql = """ + SELECT u.login AS friend_login + FROM connections_state cs + JOIN solana_users u + ON u.login = cs.to_login COLLATE NOCASE + WHERE cs.login = ? COLLATE NOCASE + AND cs.rel_type = ? + ORDER BY u.login + """; + + List out = new ArrayList<>(); + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, loginAnyCase); + ps.setInt(2, relType); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String v = rs.getString("friend_login"); + if (v != null) out.add(v); + } + } + } + return out; + } + + /** + * Incoming: список логинов (канонических), кто поставил relType пользователю login. + */ + public List listIncomingByRelTypeCanonical(Connection c, String loginAnyCase, int relType) throws SQLException { + String sql = """ + SELECT u.login AS friend_login + FROM connections_state cs + JOIN solana_users u + ON u.login = cs.login COLLATE NOCASE + WHERE cs.to_login = ? COLLATE NOCASE + AND cs.rel_type = ? + ORDER BY u.login + """; + + List out = new ArrayList<>(); + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, loginAnyCase); + ps.setInt(2, relType); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String v = rs.getString("friend_login"); + if (v != null) out.add(v); + } + } + } + return out; + } + + /** + * Mutual: список логинов (канонических), у кого дружба в обе стороны. + */ + public List listMutualByRelTypeCanonical(Connection c, String loginAnyCase, int relType) throws SQLException { + String sql = """ + SELECT u.login AS friend_login + FROM connections_state a + JOIN solana_users u + ON u.login = a.to_login COLLATE NOCASE + WHERE a.login = ? COLLATE NOCASE + AND a.rel_type = ? + AND EXISTS ( + SELECT 1 + FROM connections_state b + WHERE b.login = a.to_login COLLATE NOCASE + AND b.to_login = a.login COLLATE NOCASE + AND b.rel_type = a.rel_type + ) + ORDER BY u.login + """; + + List out = new ArrayList<>(); + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, loginAnyCase); + ps.setInt(2, relType); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String v = rs.getString("friend_login"); + if (v != null) out.add(v); + } + } + } + return out; + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index a2b2df8..86540ba 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -42,10 +42,14 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserPar import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request; import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request; -// !!! подставь реальные пакеты/имена, как у тебя в проекте: +// --- subscriptions --- //import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler; import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request; +// --- NEW: connections friends lists --- +import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler; +import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request; + import java.util.Map; /** @@ -54,7 +58,6 @@ import java.util.Map; */ public final class JsonHandlerRegistry { - // Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...) private static final Map HANDLERS = Map.ofEntries( Map.entry("AddUser", new Net_AddUser_Handler()), Map.entry("GetUser", new Net_GetUser_Handler()), @@ -76,7 +79,10 @@ public final class JsonHandlerRegistry { // --- userParams --- Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()), Map.entry("GetUserParam", new Net_GetUserParam_Handler()), - Map.entry("ListUserParams", new Net_ListUserParams_Handler()) + Map.entry("ListUserParams", new Net_ListUserParams_Handler()), + + // --- connections --- + Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()) // --- subscriptions --- // Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler()) @@ -106,7 +112,10 @@ public final class JsonHandlerRegistry { Map.entry("ListUserParams", Net_ListUserParams_Request.class), // --- subscriptions --- - Map.entry("ListSubscribedChannels", Net_GetSubscribedChannels_Request.class) + Map.entry("ListSubscribedChannels", Net_GetSubscribedChannels_Request.class), + + // --- connections --- + Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class) ); private JsonHandlerRegistry() { } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetFriendsLists_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetFriendsLists_Handler.java new file mode 100644 index 0000000..609310c --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetFriendsLists_Handler.java @@ -0,0 +1,117 @@ +package server.logic.ws_protocol.JSON.handlers.connections; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request; +import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.MsgSubType; +import shine.db.SqliteDbController; +import shine.db.dao.ConnectionsStateDAO; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.List; + +/** + * GetFriendsLists — получить 3 списка (для того что бы построить граф связей друзей пользователя): + * - out_friends: кому login поставил FRIEND + * - in_friends: кто поставил FRIEND этому login + * - mutual_friends: FRIEND в обе стороны + * + * ВАЖНО: + * - login в запросе может быть любым регистром + * - в ответе возвращаем канонический регистр (как в solana_users.login) + * + * ПРИМЕЧАНИЕ: + * Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL. + */ +public class Net_GetFriendsLists_Handler implements JsonMessageHandler { + + private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest; + + if (req.getLogin() == null || req.getLogin().isBlank()) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_FIELDS", + "Некорректные поля: login" + ); + } + + final String loginAnyCase = req.getLogin().trim(); + + try { + SqliteDbController db = SqliteDbController.getInstance(); + ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance(); + + try (Connection c = db.getConnection()) { + + // 1) Канонизируем login через solana_users (NOCASE) + String canonicalLogin = findCanonicalLogin(c, loginAnyCase); + if (canonicalLogin == null) { + return NetExceptionResponseFactory.error( + req, + 404, + "USER_NOT_FOUND", + "Пользователь не найден" + ); + } + + int relType = (int) MsgSubType.CONNECTION_FRIEND; + + // 2) Три списка (все логины канонические) + List outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType); + List inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType); + List mutual = dao.listMutualByRelTypeCanonical(c, canonicalLogin, relType); + + Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + + resp.setLogin(canonicalLogin); + resp.setOut_friends(outFriends); + resp.setIn_friends(inFriends); + resp.setMutual_friends(mutual); + + return resp; + } + + } catch (Exception e) { + log.error("❌ Internal error GetFriendsLists", e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.INTERNAL_ERROR, + "INTERNAL_ERROR", + "Внутренняя ошибка сервера" + ); + } + } + + private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception { + String sql = """ + SELECT login + FROM solana_users + WHERE login = ? COLLATE NOCASE + LIMIT 1 + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, loginAnyCase); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + return rs.getString("login"); + } + } + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetFriendsLists_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetFriendsLists_Request.java new file mode 100644 index 0000000..a661e1c --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetFriendsLists_Request.java @@ -0,0 +1,29 @@ +package server.logic.ws_protocol.JSON.handlers.connections.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +/** + * Запрос GetFriendsLists — получить два списка "друзей" по connections_state. + * + * { + * "op": "GetFriendsLists", + * "requestId": "req-100", + * "payload": { + * "login": "anya" + * } + * } + * + * Возвращает: + * - out_friends: кому login поставил FRIEND + * - in_friends: кто поставил FRIEND этому login + * + * ПРО ДОСТУП (на будущее): + * Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей. + */ +public class Net_GetFriendsLists_Request extends Net_Request { + + private String login; + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetFriendsLists_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetFriendsLists_Response.java new file mode 100644 index 0000000..4218d34 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetFriendsLists_Response.java @@ -0,0 +1,42 @@ +package server.logic.ws_protocol.JSON.handlers.connections.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +import java.util.ArrayList; +import java.util.List; + +/** + * Ответ GetFriendsLists. + * + * { + * "op": "GetFriendsLists", + * "requestId": "req-100", + * "status": 200, + * "payload": { + * "login": "Anya", // канонический регистр из БД + * "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND + * "in_friends": ["Alex", "Kate"], // кто поставил FRIEND login + * "mutual_friends": ["Kate"] // взаимные (два направления) + * } + * } + */ +public class Net_GetFriendsLists_Response extends Net_Response { + + private String login; + + private List out_friends = new ArrayList<>(); + private List in_friends = new ArrayList<>(); + private List mutual_friends = new ArrayList<>(); + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + + public List getOut_friends() { return out_friends; } + public void setOut_friends(List out_friends) { this.out_friends = out_friends; } + + public List getIn_friends() { return in_friends; } + public void setIn_friends(List in_friends) { this.in_friends = in_friends; } + + public List getMutual_friends() { return mutual_friends; } + public void setMutual_friends(List mutual_friends) { this.mutual_friends = mutual_friends; } +} \ No newline at end of file diff --git a/src/test/java/test/it/runner/IT_RunAllMain.java b/src/test/java/test/it/runner/IT_RunAllMain.java index fb5fca2..865f081 100644 --- a/src/test/java/test/it/runner/IT_RunAllMain.java +++ b/src/test/java/test/it/runner/IT_RunAllMain.java @@ -4,6 +4,7 @@ import test.it.cases.IT_01_AddUser; import test.it.cases.IT_02_Sessions; import test.it.cases.IT_03_AddBlock_NoAuth; import test.it.cases.IT_04_UserParams_NoAuth; +import test.it.cases.IT_05_Connections_NoAuth; import test.it.utils.log.TestLog; import java.util.ArrayList; @@ -48,6 +49,9 @@ public class IT_RunAllMain { String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4); if (s4.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); } + String s5 = IT_05_Connections_NoAuth.run(); summaries.add(s5); + if (s5.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); } + return finish(summaries, failed); } @@ -65,4 +69,4 @@ public class IT_RunAllMain { return failed; } -} +} \ No newline at end of file diff --git a/src/test/java/test/it/utils/json/JsonBuilders.java b/src/test/java/test/it/utils/json/JsonBuilders.java index 11dbb5b..eec0d4b 100644 --- a/src/test/java/test/it/utils/json/JsonBuilders.java +++ b/src/test/java/test/it/utils/json/JsonBuilders.java @@ -75,6 +75,21 @@ public final class JsonBuilders { """.formatted(requestId, prefix); } + // ---------------- GetFriendsLists ---------------- + + public static String getFriendsLists(String login) { + String requestId = TestIds.next("friends"); + return """ + { + "op": "GetFriendsLists", + "requestId": "%s", + "payload": { + "login": "%s" + } + } + """.formatted(requestId, login); + } + // ---------------- AuthChallenge ---------------- public static String authChallenge(String login) { diff --git a/src/test/java/test/it/utils/json/JsonParsers.java b/src/test/java/test/it/utils/json/JsonParsers.java index 5d21716..1eb1e5a 100644 --- a/src/test/java/test/it/utils/json/JsonParsers.java +++ b/src/test/java/test/it/utils/json/JsonParsers.java @@ -166,6 +166,42 @@ public final class JsonParsers { return res; } + // ---------------- Friends helpers ---------------- + + /** payload.login (канонический) */ + public static String friendsLogin(String json) { + return getPayloadText(json, "login"); + } + + public static List friendsOut(String json) { + return getPayloadStringArray(json, "out_friends"); + } + + public static List friendsIn(String json) { + return getPayloadStringArray(json, "in_friends"); + } + + public static List friendsMutual(String json) { + return getPayloadStringArray(json, "mutual_friends"); + } + + private static List getPayloadStringArray(String json, String field) { + List res = new ArrayList<>(); + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload == null) return res; + + JsonNode arr = payload.get(field); + if (arr == null || !arr.isArray()) return res; + + for (JsonNode x : arr) { + if (x != null && !x.isNull()) res.add(x.asText()); + } + } catch (Exception ignored) {} + return res; + } + private static String getPayloadText(String json, String field) { try { JsonNode root = MAPPER.readTree(json);