User-Flow, поиск, редактирование информации о себе

This commit is contained in:
RoyceDa
2026-02-05 01:22:39 +02:00
parent f74f4e7af7
commit 4a4cd81891
14 changed files with 415 additions and 13 deletions

View File

@@ -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() {

View File

@@ -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<String, Object> getIndex() {
return null;
Map<String, Object> indexes = new HashMap<>();
if(this.hasAuthorized()){
/**
* Индексируем пользователя только если он авторизован,
* иначе не нужно их индексировать, чтобы не забивать память
*/
indexes.put("publicKey", publicKey);
}
return indexes;
}
}

View File

@@ -0,0 +1,26 @@
package com.rosetta.im.database;
import org.hibernate.Session;
import org.hibernate.query.Query;
public class QuerySession<T> implements AutoCloseable {
private Session session;
private Query<T> query;
public QuerySession(Session session, Query<T> query) {
this.session = session;
this.query = query;
}
public Query<T> getQuery() {
return query;
}
@Override
public void close() {
if (session != null && session.isOpen()) {
session.close();
}
}
}

View File

@@ -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<T> {
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<T> 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<T> {
});
}
/**
* Выполняет запрос с параметрами и возвращает список сущностей
* @param queryString SQL запрос
* @param parameters параметры запроса
* @return список сущностей
*/
public QuerySession<T> buildQuery(String queryString, HashMap<String, Object> parameters) {
Session session = HibernateUtil.openSession();
try {
Query<T> 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 карта полей и их значений

View File

@@ -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<Packet1UserInfo> {
private final UserRepository userRepository = new UserRepository();
private final UserService userService = new UserService(userRepository);
private final HashSet<String> 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;
}
}

View File

@@ -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<Packet3Search> {
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<Packet3Search> {
return;
}
if(search.trim().equals("")){
/**
* Пустой поисковой запрос
*/
return;
}
List<User> usersFindedList = userService.searchUsers(search, 7);
Packet3Search response = new Packet3Search();
response.setSearch("");
response.setPrivateKey("");
List<SearchInfo> 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);
}
}

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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<SearchInfo> getSearchInfos() {
return this.searchInfo;
}
/**
* Устанавливает результаты поиска
* @param searchInfo
*/
public void setSearchInfos(List<SearchInfo> searchInfos) {
this.searchInfo = searchInfos;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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 Имя пользователя.

View File

@@ -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<UserRepository> {
public UserService(UserRepository repository) {
@@ -13,12 +18,46 @@ public class UserService extends Service<UserRepository> {
}
/**
* Поиск пользователей по части имени пользователя.
* Поиск пользователей по части имени пользователя и публичному ключу.
* @param query часть имени пользователя
* @param take сколько пользователей отдать
* @return список пользователей, соответствующих запросу
*/
public List<User> searchUsers(String query) {
return getRepository().likeSearchAll("username", query);
public List<User> searchUsers(String query, int take) {
String hql = "FROM User WHERE username LIKE :query OR publicKey = :queryExact ORDER BY verified ASC";
HashMap<String, Object> parameters = new HashMap<>();
parameters.put("query", "%" + query + "%");
parameters.put("queryExact", query);
try(QuerySession<User> 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;
}
}

View File

@@ -68,6 +68,7 @@ public class Server extends WebSocketServer {
@Override
public void onError(WebSocket arg0, Exception arg1) {
arg1.printStackTrace();
if(this.listener == null){
return;
}