From 09566fdfde1f4305527216642a444fe9d214486e3f095a494cb4271827c1fea0 Mon Sep 17 00:00:00 2001 From: ai5590 Date: Sun, 5 Apr 2026 12:12:55 +0300 Subject: [PATCH] Add close-friend flow on network tab with server API --- shine-UI/js/pages/network-view.js | 69 ++++++++++++++- shine-UI/js/services/auth-service.js | 7 ++ .../shine/db/dao/ConnectionsStateDAO.java | 30 ++++++- .../ws_protocol/JSON/JsonHandlerRegistry.java | 4 + .../Net_AddCloseFriend_Handler.java | 85 +++++++++++++++++++ .../entyties/Net_AddCloseFriend_Request.java | 10 +++ .../entyties/Net_AddCloseFriend_Response.java | 16 ++++ 7 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_AddCloseFriend_Handler.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Response.java diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js index 6d88038..fd30c3d 100644 --- a/shine-UI/js/pages/network-view.js +++ b/shine-UI/js/pages/network-view.js @@ -10,6 +10,65 @@ function makeNode(name, cls = '') { return n; } +function showAddCloseFriendModal({ onAdded }) { + const root = document.getElementById('modal-root'); + root.innerHTML = ` + + `; + + const close = () => { root.innerHTML = ''; }; + root.querySelector('#close-friend-back').addEventListener('click', close); + + root.querySelector('#close-friend-search').addEventListener('click', async () => { + const query = root.querySelector('#close-friend-query').value.trim(); + const holder = root.querySelector('#close-friend-results'); + holder.innerHTML = '

Поиск...

'; + + try { + const logins = await authService.searchUsers(query); + holder.innerHTML = ''; + if (!logins.length) { + holder.innerHTML = '

Пользователи не найдены.

'; + return; + } + + logins.forEach((login) => { + const row = document.createElement('article'); + row.className = 'list-item'; + row.innerHTML = ` +
${(login[0] || '?').toUpperCase()}
+
${login}

Пользователь

+
Добавить
+ `; + row.addEventListener('click', async () => { + const yes = window.confirm(`Добавить ${login} в близкие друзья?`); + if (!yes) return; + try { + await authService.addCloseFriend(login); + close(); + if (typeof onAdded === 'function') await onAdded(); + } catch (e) { + window.alert(`Ошибка добавления: ${e.message || 'unknown'}`); + } + }); + holder.append(row); + }); + } catch (e) { + holder.innerHTML = `

Ошибка поиска: ${e.message || 'unknown'}

`; + } + }); +} + export function render() { const screen = document.createElement('section'); screen.className = 'stack'; @@ -62,14 +121,20 @@ export function render() { right.forEach((name, i) => svg.append(mk(name, 'right', i, right.length))); board.prepend(svg); - note.textContent = 'Нажмите на любой узел, чтобы построить связи выбранного пользователя.'; + note.textContent = 'Нажмите на узел, чтобы перестроить связи вокруг выбранного пользователя.'; } catch (e) { note.textContent = `Ошибка загрузки связей: ${e.message || 'unknown'}`; } }; + const addBtn = document.createElement('button'); + addBtn.className = 'primary-btn'; + addBtn.type = 'button'; + addBtn.textContent = 'Добавить близкого друга'; + addBtn.addEventListener('click', () => showAddCloseFriendModal({ onAdded: () => load() })); + load(); - screen.append(renderHeader({ title: 'Связи' }), board, note); + screen.append(renderHeader({ title: 'Связи' }), addBtn, board, note); return screen; } diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 00f79f7..bd2afb5 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -264,6 +264,13 @@ export class AuthService { return response.payload || {}; } + + async addCloseFriend(toLogin) { + const response = await this.ws.request('AddCloseFriend', { toLogin }); + if (response.status !== 200) throw opError('AddCloseFriend', response); + return response.payload || {}; + } + async getUserConnectionsGraph(login) { const response = await this.ws.request('GetUserConnectionsGraph', { login }); if (response.status !== 200) throw opError('GetUserConnectionsGraph', response); 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 index 3d03346..1e910bf 100644 --- a/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java @@ -125,4 +125,32 @@ public final class ConnectionsStateDAO { } return out; } -} \ No newline at end of file + + public void upsertRelation(Connection c, + String login, + int relType, + String toLogin, + String toBchName, + Integer toBlockNumber, + byte[] toBlockHash) throws SQLException { + String sql = """ + INSERT INTO connections_state (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(login, rel_type, to_login) DO UPDATE SET + to_bch_name=excluded.to_bch_name, + to_block_number=excluded.to_block_number, + to_block_hash=excluded.to_block_hash + """; + + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, login); + ps.setInt(2, relType); + ps.setString(3, toLogin); + ps.setString(4, toBchName); + if (toBlockNumber == null) ps.setNull(5, java.sql.Types.INTEGER); else ps.setInt(5, toBlockNumber); + ps.setBytes(6, toBlockHash); + ps.executeUpdate(); + } + } + +} 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 604f7b7..5862635 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 @@ -52,8 +52,10 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMe import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request; import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request; import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler; +import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler; import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler; import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request; +import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request; import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request; import server.logic.ws_protocol.JSON.messages.Net_AckIncomingMessage_Handler; import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler; @@ -108,6 +110,7 @@ public final class JsonHandlerRegistry { Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()), Map.entry("ListContacts", new Net_ListContacts_Handler()), Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()), + Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()), // --- direct messages / push --- Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()), @@ -153,6 +156,7 @@ public final class JsonHandlerRegistry { Map.entry("GetMessageThread", Net_GetMessageThread_Request.class), Map.entry("ListContacts", Net_ListContacts_Request.class), Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class), + Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class), // --- direct messages / push --- Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class), diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_AddCloseFriend_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_AddCloseFriend_Handler.java new file mode 100644 index 0000000..3cfe34f --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_AddCloseFriend_Handler.java @@ -0,0 +1,85 @@ +package server.logic.ws_protocol.JSON.handlers.connections; + +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_AddCloseFriend_Request; +import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.MsgSubType; +import shine.db.dao.ConnectionsStateDAO; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class Net_AddCloseFriend_Handler implements JsonMessageHandler { + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception { + Net_AddCloseFriend_Request req = (Net_AddCloseFriend_Request) baseRequest; + if (ctx == null || !ctx.isAuthenticatedUser()) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация"); + } + if (req.getToLogin() == null || req.getToLogin().isBlank()) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin обязателен"); + } + + String from = ctx.getLogin(); + String toLogin = req.getToLogin().trim(); + if (from.equalsIgnoreCase(toLogin)) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Нельзя добавить себя"); + } + + try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) { + String canonicalTo = findCanonicalLogin(c, toLogin); + if (canonicalTo == null) { + return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден"); + } + String targetBch = findPrimaryBlockchain(c, canonicalTo); + if (targetBch == null) { + return NetExceptionResponseFactory.error(req, 404, "BLOCKCHAIN_NOT_FOUND", "У пользователя нет blockchain"); + } + + ConnectionsStateDAO.getInstance().upsertRelation( + c, + from, + MsgSubType.CONNECTION_FRIEND, + canonicalTo, + targetBch, + 0, + new byte[32] + ); + + Net_AddCloseFriend_Response resp = new Net_AddCloseFriend_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + resp.setLogin(from); + resp.setToLogin(canonicalTo); + resp.setRelation("close_friend"); + return resp; + } + } + + private String findCanonicalLogin(Connection c, String login) throws Exception { + String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, login); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getString("login") : null; + } + } + } + + private String findPrimaryBlockchain(Connection c, String login) throws Exception { + String sql = "SELECT blockchain_name FROM blockchain_state WHERE login=? ORDER BY blockchain_name LIMIT 1"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, login); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getString("blockchain_name") : null; + } + } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Request.java new file mode 100644 index 0000000..76caef3 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Request.java @@ -0,0 +1,10 @@ +package server.logic.ws_protocol.JSON.handlers.connections.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +public class Net_AddCloseFriend_Request extends Net_Request { + private String toLogin; + + public String getToLogin() { return toLogin; } + public void setToLogin(String toLogin) { this.toLogin = toLogin; } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Response.java new file mode 100644 index 0000000..5e5b07e --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Response.java @@ -0,0 +1,16 @@ +package server.logic.ws_protocol.JSON.handlers.connections.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +public class Net_AddCloseFriend_Response extends Net_Response { + private String login; + private String toLogin; + private String relation; + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + public String getToLogin() { return toLogin; } + public void setToLogin(String toLogin) { this.toLogin = toLogin; } + public String getRelation() { return relation; } + public void setRelation(String relation) { this.relation = relation; } +}