From 4a4cd81891a0af56d5e5be307e48550cb46203ba Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 5 Feb 2026 01:22:39 +0200 Subject: [PATCH] =?UTF-8?q?User-Flow,=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA,=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BE=20=D1=81=D0=B5=D0=B1=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/rosetta/im/Boot.java | 7 +- .../im/client/tags/ECIAuthentificate.java | 11 +- .../com/rosetta/im/database/QuerySession.java | 26 +++ .../com/rosetta/im/database/Repository.java | 39 +++- .../im/executors/Executor1UserInfo.java | 180 ++++++++++++++++++ .../rosetta/im/executors/Executor3Search.java | 40 ++++ .../rosetta/im/packet/Packet1UserInfo.java | 1 - .../com/rosetta/im/packet/Packet2Result.java | 12 +- .../com/rosetta/im/packet/Packet3Search.java | 17 ++ .../im/packet/runtime/NetworkStatus.java | 12 +- .../rosetta/im/packet/runtime/ResultCode.java | 27 +++ .../rosetta/im/packet/runtime/SearchInfo.java | 10 + .../im/service/services/UserService.java | 45 ++++- src/main/java/io/orprotocol/Server.java | 1 + 14 files changed, 415 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/rosetta/im/database/QuerySession.java create mode 100644 src/main/java/com/rosetta/im/executors/Executor1UserInfo.java create mode 100644 src/main/java/com/rosetta/im/packet/runtime/ResultCode.java diff --git a/src/main/java/com/rosetta/im/Boot.java b/src/main/java/com/rosetta/im/Boot.java index 728507a..1bf8ca6 100644 --- a/src/main/java/com/rosetta/im/Boot.java +++ b/src/main/java/com/rosetta/im/Boot.java @@ -3,6 +3,8 @@ package com.rosetta.im; import com.rosetta.im.client.ClientManager; import com.rosetta.im.event.EventManager; import com.rosetta.im.executors.Executor0Handshake; +import com.rosetta.im.executors.Executor1UserInfo; +import com.rosetta.im.executors.Executor3Search; import com.rosetta.im.listeners.HandshakeCompleteListener; import com.rosetta.im.listeners.ServerStopListener; import com.rosetta.im.logger.Logger; @@ -75,10 +77,11 @@ public class Boot { } /** - * Регистрация пакетов, обработчиков, событий приложения + * Запуск сервера, регистрация пакетов, обработчиков, событий приложения * @return Boot */ public Boot bootstrap() { + this.server.start(); this.registerAllPackets(); this.registerAllExecutors(); this.registerAllEvents(); @@ -100,6 +103,8 @@ public class Boot { private void registerAllExecutors() { this.packetManager.registerExecutor(0, new Executor0Handshake(this.eventManager)); + this.packetManager.registerExecutor(1, new Executor1UserInfo()); + this.packetManager.registerExecutor(3, new Executor3Search(this.clientManager)); } private void printBootMessage() { diff --git a/src/main/java/com/rosetta/im/client/tags/ECIAuthentificate.java b/src/main/java/com/rosetta/im/client/tags/ECIAuthentificate.java index bc08482..fd15864 100644 --- a/src/main/java/com/rosetta/im/client/tags/ECIAuthentificate.java +++ b/src/main/java/com/rosetta/im/client/tags/ECIAuthentificate.java @@ -1,5 +1,6 @@ package com.rosetta.im.client.tags; +import java.util.HashMap; import java.util.Map; import com.rosetta.im.packet.runtime.HandshakeStage; @@ -58,7 +59,15 @@ public class ECIAuthentificate implements ECITag { */ @Override public Map getIndex() { - return null; + Map indexes = new HashMap<>(); + if(this.hasAuthorized()){ + /** + * Индексируем пользователя только если он авторизован, + * иначе не нужно их индексировать, чтобы не забивать память + */ + indexes.put("publicKey", publicKey); + } + return indexes; } } diff --git a/src/main/java/com/rosetta/im/database/QuerySession.java b/src/main/java/com/rosetta/im/database/QuerySession.java new file mode 100644 index 0000000..9b64f0f --- /dev/null +++ b/src/main/java/com/rosetta/im/database/QuerySession.java @@ -0,0 +1,26 @@ +package com.rosetta.im.database; + +import org.hibernate.Session; +import org.hibernate.query.Query; + +public class QuerySession implements AutoCloseable { + + private Session session; + private Query query; + + public QuerySession(Session session, Query query) { + this.session = session; + this.query = query; + } + + public Query getQuery() { + return query; + } + + @Override + public void close() { + if (session != null && session.isOpen()) { + session.close(); + } + } +} diff --git a/src/main/java/com/rosetta/im/database/Repository.java b/src/main/java/com/rosetta/im/database/Repository.java index 2b2398d..3ef5d7c 100644 --- a/src/main/java/com/rosetta/im/database/Repository.java +++ b/src/main/java/com/rosetta/im/database/Repository.java @@ -5,6 +5,7 @@ import java.util.List; import org.hibernate.Session; import org.hibernate.Transaction; +import org.hibernate.query.Query; /** * Базовый репозиторий для работы с сущностями базы данных @@ -92,11 +93,27 @@ public abstract class Repository { return executeInSession(session -> { String queryString = "FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " LIKE :value"; return session.createQuery(queryString, entityClass) - .setParameter("value", "%" + value + "%") + .setParameter("value", value + "%") .list(); }); } + /** + * Поиск сущности по значению одного поля с использованием оператора LIKE и ограничения LIMIT + * @param fieldName поле + * @param value значение + * @return найденная сущность или null + */ + public List likeSearchAll(String fieldName, String value, int take) { + return executeInSession(session -> { + String queryString = "FROM " + entityClass.getSimpleName() + " WHERE " + fieldName + " LIKE :value"; + return session.createQuery(queryString, entityClass) + .setParameter("value", value + "%") + .setMaxResults(take) + .list(); + }); + } + /** * Поиск сущности по набору полей * @param fields карта полей и их значений @@ -212,6 +229,26 @@ public abstract class Repository { }); } + /** + * Выполняет запрос с параметрами и возвращает список сущностей + * @param queryString SQL запрос + * @param parameters параметры запроса + * @return список сущностей + */ + public QuerySession buildQuery(String queryString, HashMap parameters) { + Session session = HibernateUtil.openSession(); + try { + Query query = session.createQuery(queryString, entityClass); + for (var entry : parameters.entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + return new QuerySession<>(session, query); + } catch (Exception e) { + session.close(); + throw e; + } + } + /** * Подсчет сущностей по набору полей * @param fields карта полей и их значений diff --git a/src/main/java/com/rosetta/im/executors/Executor1UserInfo.java b/src/main/java/com/rosetta/im/executors/Executor1UserInfo.java new file mode 100644 index 0000000..586b539 --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor1UserInfo.java @@ -0,0 +1,180 @@ +package com.rosetta.im.executors; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.rosetta.im.Failures; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.entity.User; +import com.rosetta.im.database.repository.UserRepository; +import com.rosetta.im.packet.Packet1UserInfo; +import com.rosetta.im.packet.Packet2Result; +import com.rosetta.im.packet.runtime.ResultCode; +import com.rosetta.im.service.services.UserService; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.packet.PacketExecutor; + +public class Executor1UserInfo extends PacketExecutor { + + private final UserRepository userRepository = new UserRepository(); + private final UserService userService = new UserService(userRepository); + private final HashSet blockedUsernames = new HashSet<>(Arrays.asList( + "user", + "admin", + "rosettasupport", + "rosettaupdates", + "freddie871", + "updates", + "deleted", + "safety", + "secure", + "rosettasafe" + )); + + @Override + public void onPacketReceived(Packet1UserInfo packet, Client client) throws Exception, ProtocolException { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + String username = packet.getUsername(); + String title = packet.getTitle(); + + if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){ + /** + * Только для авторизованных пользователей, а этот пользователь - не авторизован + */ + client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); + return; + } + + User user = userService.fromClient(client); + if(user == null){ + /** + * Пользователь с таким ключем не найден в базе, + * такого не может быть, но лучше чтобы была дополнительная проверка + */ + client.disconnect(Failures.DATA_MISSMATCH); + return; + } + + ResultCode usernameResult = tryChangeUsername(user, username); + if(usernameResult == ResultCode.USERNAME_TAKEN){ + /** + * Это имя пользователя уже занято, отправляем клиенту ошибку + */ + Packet2Result result = new Packet2Result(); + result.setResultCode(ResultCode.USERNAME_TAKEN); + client.send(result); + return; + } + if(usernameResult != ResultCode.SUCCESS){ + /** + * Не удалось сменить username, отправляем клиенту ошибку + */ + Packet2Result result = new Packet2Result(); + result.setResultCode(ResultCode.INVALID); + client.send(result); + return; + } + + ResultCode titleResult = tryChangeTitle(user, title); + if(titleResult != ResultCode.SUCCESS){ + /** + * Не удалось сменить title, отправляем клиенту ошибку + */ + Packet2Result result = new Packet2Result(); + result.setResultCode(ResultCode.INVALID); + client.send(result); + return; + } + + /** + * Отправляем клиенту успешный результат + */ + Packet2Result result = new Packet2Result(); + result.setResultCode(ResultCode.SUCCESS); + client.send(result); + } + + /** + * Пробует сменить username + * @param user пользователь + * @param username имя пользователя для смены + * @return вернет false если смена прошла неудачно или true если username + * не нуждается в изменении или изменен + */ + public ResultCode tryChangeUsername(User user, String username){ + String targetRegexp = "^[a-z][a-z0-9_]{4,15}$"; + Pattern pattern = Pattern.compile(targetRegexp); + Matcher matcher = pattern.matcher(username); + + if(user.getUsername().equalsIgnoreCase(username)){ + /** + * Пользователь не меняет имя, значит операция прошла успешно, + * по крайней мере нам не нужно возвращать клиенту код ошибки + */ + return ResultCode.SUCCESS; + } + if(!matcher.matches()){ + /** + * Не подходит по регулярному выражению + */ + return ResultCode.INVALID; + } + if(blockedUsernames.contains(username)){ + /** + * Это имя пользователя не доступно для смены + */ + return ResultCode.INVALID; + } + if(userService.isUsernameTaken(username)){ + /** + * Такое имя пользователя уже занято + */ + return ResultCode.USERNAME_TAKEN; + } + + /** + * Меняем имя пользователя + */ + user.setUsername(username); + userRepository.update(user); + return ResultCode.SUCCESS; + } + + /** + * Пробует сменить заголовок пользователя (title) + * @param user пользователь + * @param username имя пользователя для смены + * @return вернет false если смена прошла неудачно или true если title + * не нуждается в изменении или изменен + */ + public ResultCode tryChangeTitle(User user, String title) { + String targetRegexp = "^[a-zA-Z0-9а-яА-Я _-]{1,22}$"; + Pattern pattern = Pattern.compile(targetRegexp); + Matcher matcher = pattern.matcher(title); + + if(user.getTitle().equalsIgnoreCase(title)){ + /** + * Пользователь не меняет имя, значит операция прошла успешно, + * по крайней мере нам не нужно возвращать клиенту код ошибки + */ + return ResultCode.SUCCESS; + } + if(!matcher.matches()){ + /** + * Не подходит по регулярному выражению + */ + return ResultCode.INVALID; + } + /** + * Меняем имя пользователя + */ + user.setTitle(title); + userRepository.update(user); + return ResultCode.SUCCESS; + } + +} diff --git a/src/main/java/com/rosetta/im/executors/Executor3Search.java b/src/main/java/com/rosetta/im/executors/Executor3Search.java index 4f07380..20e1533 100644 --- a/src/main/java/com/rosetta/im/executors/Executor3Search.java +++ b/src/main/java/com/rosetta/im/executors/Executor3Search.java @@ -1,9 +1,17 @@ package com.rosetta.im.executors; +import java.util.ArrayList; +import java.util.List; + import com.rosetta.im.Failures; +import com.rosetta.im.client.ClientManager; import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.entity.User; import com.rosetta.im.database.repository.UserRepository; import com.rosetta.im.packet.Packet3Search; +import com.rosetta.im.packet.runtime.NetworkStatus; +import com.rosetta.im.packet.runtime.SearchInfo; +import com.rosetta.im.service.services.UserService; import io.orprotocol.ProtocolException; import io.orprotocol.client.Client; @@ -12,6 +20,12 @@ import io.orprotocol.packet.PacketExecutor; public class Executor3Search extends PacketExecutor { private final UserRepository userRepository = new UserRepository(); + private final UserService userService = new UserService(userRepository); + private final ClientManager clientManager; + + public Executor3Search(ClientManager clientManager) { + this.clientManager = clientManager; + } @Override public void onPacketReceived(Packet3Search packet, Client client) throws Exception, ProtocolException { @@ -26,7 +40,33 @@ public class Executor3Search extends PacketExecutor { return; } + if(search.trim().equals("")){ + /** + * Пустой поисковой запрос + */ + return; + } + + List usersFindedList = userService.searchUsers(search, 7); + Packet3Search response = new Packet3Search(); + response.setSearch(""); + response.setPrivateKey(""); + + List searchInfos = new ArrayList<>(); + for(User user : usersFindedList){ + SearchInfo searchInfo = new SearchInfo( + user.getUsername(), + user.getTitle(), + user.getPublicKey(), + user.getVerified(), + NetworkStatus.fromBoolean(this.clientManager.isClientConnected(user.getPublicKey())) + ); + searchInfos.add(searchInfo); + } + + response.setSearchInfos(searchInfos); + client.send(response); } } diff --git a/src/main/java/com/rosetta/im/packet/Packet1UserInfo.java b/src/main/java/com/rosetta/im/packet/Packet1UserInfo.java index a6022da..1170586 100644 --- a/src/main/java/com/rosetta/im/packet/Packet1UserInfo.java +++ b/src/main/java/com/rosetta/im/packet/Packet1UserInfo.java @@ -13,7 +13,6 @@ public class Packet1UserInfo extends Packet { @Override public void read(Stream stream) { - this.packetId = stream.readInt16(); this.username = stream.readString(); this.title = stream.readString(); this.privateKey = stream.readString(); diff --git a/src/main/java/com/rosetta/im/packet/Packet2Result.java b/src/main/java/com/rosetta/im/packet/Packet2Result.java index d5c4056..cd327a1 100644 --- a/src/main/java/com/rosetta/im/packet/Packet2Result.java +++ b/src/main/java/com/rosetta/im/packet/Packet2Result.java @@ -1,22 +1,24 @@ package com.rosetta.im.packet; +import com.rosetta.im.packet.runtime.ResultCode; + import io.orprotocol.Stream; import io.orprotocol.packet.Packet; public class Packet2Result extends Packet { - private int resultCode; + private ResultCode resultCode; @Override public void read(Stream stream) { - this.resultCode = stream.readInt8(); + this.resultCode = ResultCode.fromCode(stream.readInt16()); } @Override public Stream write() { Stream stream = new Stream(); stream.writeInt16(this.packetId); - stream.writeInt8(this.resultCode); + stream.writeInt16(this.resultCode.getCode()); return stream; } @@ -24,7 +26,7 @@ public class Packet2Result extends Packet { * Получает код результата операции * @return код результата */ - public int getResultCode() { + public ResultCode getResultCode() { return this.resultCode; } @@ -32,7 +34,7 @@ public class Packet2Result extends Packet { * Устанавливает код результата операции * @param resultCode код результата */ - public void setResultCode(int resultCode) { + public void setResultCode(ResultCode resultCode) { this.resultCode = resultCode; } diff --git a/src/main/java/com/rosetta/im/packet/Packet3Search.java b/src/main/java/com/rosetta/im/packet/Packet3Search.java index cd18c5b..683160b 100644 --- a/src/main/java/com/rosetta/im/packet/Packet3Search.java +++ b/src/main/java/com/rosetta/im/packet/Packet3Search.java @@ -58,6 +58,7 @@ public class Packet3Search extends Packet { * @deprecated с версии сервера 1.1 использование приватных ключей * в протоколе устарело, так как теперь сервер использует Handshake для аутентификации пользователей. */ + @Deprecated(since = "1.1", forRemoval = true) public void setPrivateKey(String privateKey) { this.privateKey = privateKey; } @@ -78,4 +79,20 @@ public class Packet3Search extends Packet { this.search = search; } + /** + * Получает результаты поиска + * @return список результатов + */ + public List getSearchInfos() { + return this.searchInfo; + } + + /** + * Устанавливает результаты поиска + * @param searchInfo + */ + public void setSearchInfos(List searchInfos) { + this.searchInfo = searchInfos; + } + } diff --git a/src/main/java/com/rosetta/im/packet/runtime/NetworkStatus.java b/src/main/java/com/rosetta/im/packet/runtime/NetworkStatus.java index 1ed68fe..602dfd2 100644 --- a/src/main/java/com/rosetta/im/packet/runtime/NetworkStatus.java +++ b/src/main/java/com/rosetta/im/packet/runtime/NetworkStatus.java @@ -1,8 +1,11 @@ package com.rosetta.im.packet.runtime; +/** + * Статус пользователя в сети + */ public enum NetworkStatus { ONLINE(0), - OFFILE(1); + OFFLINE(1); private final int code; @@ -22,4 +25,11 @@ public enum NetworkStatus { } throw new IllegalArgumentException("Invalid NetworkStatus code: " + code); } + + public static NetworkStatus fromBoolean(boolean status) { + if(status){ + return NetworkStatus.ONLINE; + } + return NetworkStatus.OFFLINE; + } } diff --git a/src/main/java/com/rosetta/im/packet/runtime/ResultCode.java b/src/main/java/com/rosetta/im/packet/runtime/ResultCode.java new file mode 100644 index 0000000..3cb69b0 --- /dev/null +++ b/src/main/java/com/rosetta/im/packet/runtime/ResultCode.java @@ -0,0 +1,27 @@ +package com.rosetta.im.packet.runtime; + +public enum ResultCode { + SUCCESS(0), + ERROR(1), + INVALID(2), + USERNAME_TAKEN(3); + + private final int code; + + ResultCode(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static ResultCode fromCode(int code) { + for (ResultCode rc : ResultCode.values()) { + if (rc.getCode() == code) { + return rc; + } + } + throw new IllegalArgumentException("Invalid ResultCode code: " + code); + } +} diff --git a/src/main/java/com/rosetta/im/packet/runtime/SearchInfo.java b/src/main/java/com/rosetta/im/packet/runtime/SearchInfo.java index 2b284d4..3e77bb0 100644 --- a/src/main/java/com/rosetta/im/packet/runtime/SearchInfo.java +++ b/src/main/java/com/rosetta/im/packet/runtime/SearchInfo.java @@ -11,6 +11,16 @@ public class SearchInfo implements Serializable { public int verified; public NetworkStatus networkStatus; + public SearchInfo() {} + + public SearchInfo(String username, String title, String publicKey, int verified, NetworkStatus networkStatus) { + this.username = username; + this.title = title; + this.publicKey = publicKey; + this.verified = verified; + this.networkStatus = networkStatus; + } + /** * Получает имя пользователя. * @return Имя пользователя. diff --git a/src/main/java/com/rosetta/im/service/services/UserService.java b/src/main/java/com/rosetta/im/service/services/UserService.java index 5ea2bbe..451a722 100644 --- a/src/main/java/com/rosetta/im/service/services/UserService.java +++ b/src/main/java/com/rosetta/im/service/services/UserService.java @@ -1,11 +1,16 @@ package com.rosetta.im.service.services; +import java.util.HashMap; import java.util.List; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.QuerySession; import com.rosetta.im.database.entity.User; import com.rosetta.im.database.repository.UserRepository; import com.rosetta.im.service.Service; +import io.orprotocol.client.Client; + public class UserService extends Service { public UserService(UserRepository repository) { @@ -13,12 +18,46 @@ public class UserService extends Service { } /** - * Поиск пользователей по части имени пользователя. + * Поиск пользователей по части имени пользователя и публичному ключу. * @param query часть имени пользователя + * @param take сколько пользователей отдать * @return список пользователей, соответствующих запросу */ - public List searchUsers(String query) { - return getRepository().likeSearchAll("username", query); + public List searchUsers(String query, int take) { + String hql = "FROM User WHERE username LIKE :query OR publicKey = :queryExact ORDER BY verified ASC"; + HashMap parameters = new HashMap<>(); + parameters.put("query", "%" + query + "%"); + parameters.put("queryExact", query); + try(QuerySession querySession = this.getRepository().buildQuery(hql, parameters)){ + return querySession.getQuery().setMaxResults(take).list(); + } + } + + /** + * Получает User из клиента, так же на всякий случай проверяется авторизован ли пользователь, + * если нет то User не будет найден + * @param client сетевой клиент + * @return пользователь + */ + public User fromClient(Client client) { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + if(eciAuthentificate == null){ + return null; + } + if(!eciAuthentificate.hasAuthorized()){ + return null; + } + return this.getRepository().findByField("publicKey", eciAuthentificate.getPublicKey()); + } + + /** + * Проверяет занятость имени пользователя + * @param username имя пользователя + * @return true если имя занято, иначе false + */ + public boolean isUsernameTaken(String username) { + User user = this.getRepository().findByField("username", username); + return user != null; } } diff --git a/src/main/java/io/orprotocol/Server.java b/src/main/java/io/orprotocol/Server.java index c8e995f..de4a261 100644 --- a/src/main/java/io/orprotocol/Server.java +++ b/src/main/java/io/orprotocol/Server.java @@ -68,6 +68,7 @@ public class Server extends WebSocketServer { @Override public void onError(WebSocket arg0, Exception arg1) { + arg1.printStackTrace(); if(this.listener == null){ return; }