diff --git a/src/main/java/com/rosetta/im/Boot.java b/src/main/java/com/rosetta/im/Boot.java index 39c88cb..c1c7220 100644 --- a/src/main/java/com/rosetta/im/Boot.java +++ b/src/main/java/com/rosetta/im/Boot.java @@ -10,7 +10,13 @@ import com.rosetta.im.executors.Executor10RequestUpdate; import com.rosetta.im.executors.Executor11Typeing; import com.rosetta.im.executors.Executor15RequestTransport; import com.rosetta.im.executors.Executor16PushNotification; +import com.rosetta.im.executors.Executor17GroupCreate; +import com.rosetta.im.executors.Executor18GroupInfo; +import com.rosetta.im.executors.Executor19GroupInviteInfo; import com.rosetta.im.executors.Executor1UserInfo; +import com.rosetta.im.executors.Executor20GroupJoin; +import com.rosetta.im.executors.Executor21GroupLeave; +import com.rosetta.im.executors.Executor22GroupBan; import com.rosetta.im.executors.Executor24DeviceResolve; import com.rosetta.im.executors.Executor3Search; import com.rosetta.im.executors.Executor4OnlineState; @@ -29,7 +35,7 @@ import com.rosetta.im.packet.Packet10RequestUpdate; import com.rosetta.im.packet.Packet11Typeing; import com.rosetta.im.packet.Packet15RequestTransport; import com.rosetta.im.packet.Packet16PushNotification; -import com.rosetta.im.packet.Packet17CreateGroup; +import com.rosetta.im.packet.Packet17GroupCreate; import com.rosetta.im.packet.Packet18GroupInfo; import com.rosetta.im.packet.Packet19GroupInviteInfo; import com.rosetta.im.packet.Packet1UserInfo; @@ -173,7 +179,7 @@ public class Boot { //RESERVED 14 PACKET APP UPDATE (unused) this.packetManager.registerPacket(15, Packet15RequestTransport.class); this.packetManager.registerPacket(16, Packet16PushNotification.class); - this.packetManager.registerPacket(17, Packet17CreateGroup.class); + this.packetManager.registerPacket(17, Packet17GroupCreate.class); this.packetManager.registerPacket(18, Packet18GroupInfo.class); this.packetManager.registerPacket(19, Packet19GroupInviteInfo.class); this.packetManager.registerPacket(20, Packet20GroupJoin.class); @@ -194,6 +200,12 @@ public class Boot { this.packetManager.registerExecutor(11, new Executor11Typeing(this.clientManager, this.packetManager)); this.packetManager.registerExecutor(15, new Executor15RequestTransport(this.serverConfiguration)); this.packetManager.registerExecutor(16, new Executor16PushNotification()); + this.packetManager.registerExecutor(17, new Executor17GroupCreate()); + this.packetManager.registerExecutor(18, new Executor18GroupInfo()); + this.packetManager.registerExecutor(19, new Executor19GroupInviteInfo()); + this.packetManager.registerExecutor(20, new Executor20GroupJoin()); + this.packetManager.registerExecutor(21, new Executor21GroupLeave()); + this.packetManager.registerExecutor(22, new Executor22GroupBan()); this.packetManager.registerExecutor(24, new Executor24DeviceResolve(this.clientManager, this.eventManager)); } diff --git a/src/main/java/com/rosetta/im/database/repository/GroupRepository.java b/src/main/java/com/rosetta/im/database/repository/GroupRepository.java index 4b73131..620f6f5 100644 --- a/src/main/java/com/rosetta/im/database/repository/GroupRepository.java +++ b/src/main/java/com/rosetta/im/database/repository/GroupRepository.java @@ -25,4 +25,94 @@ public class GroupRepository extends Repository { return group.getMembersPublicKeys(); } + /** + * Создать группу с заданным id и создателем, который будет единственным участником группы + * @param groupId ID группы + * @param creatorPublicKey публичный ключ создателя группы, который будет единственным участником группы при создании + */ + public void createGroup(String groupId, String creatorPublicKey) { + Group group = new Group(); + group.setGroupId(groupId); + List membersPublicKeys = new ArrayList<>(); + membersPublicKeys.add(creatorPublicKey); + group.setMembersPublicKeys(membersPublicKeys); + this.save(group); + } + + /** + * Получить группу по id + * @param groupId ID группы + * @return группа с заданным id, или null, если группа не найдена + */ + public Group getGroup(String groupId) { + return this.findByField("groupId", groupId); + } + + /** + * Удалить группу по id + * @param groupId ID группы, которую нужно удалить + */ + public void removeGroup(String groupId) { + Group group = this.findByField("groupId", groupId); + if(group != null) { + this.delete(group); + } + } + + /** + * Добавить участника в группу + * @param groupId ID группы, в которую нужно добавить участника + * @param memberPublicKey публичный ключ участника, которого нужно добавить в группу + */ + public void addMemberToGroup(String groupId, String memberPublicKey) { + Group group = this.findByField("groupId", groupId); + if(group != null) { + List membersPublicKeys = group.getMembersPublicKeys(); + if(!membersPublicKeys.contains(memberPublicKey)) { + membersPublicKeys.add(memberPublicKey); + group.setMembersPublicKeys(membersPublicKeys); + this.update(group); + } + } + } + + /** + * Удалить участника из группы + * @param groupId ID группы, из которой нужно удалить участника + * @param memberPublicKey публичный ключ участника, которого нужно удалить из группы + */ + public void removeMemberFromGroup(String groupId, String memberPublicKey) { + Group group = this.findByField("groupId", groupId); + if(group != null) { + List membersPublicKeys = group.getMembersPublicKeys(); + if(membersPublicKeys.contains(memberPublicKey)) { + membersPublicKeys.remove(memberPublicKey); + group.setMembersPublicKeys(membersPublicKeys); + this.update(group); + } + } + } + + /** + * Забанить участника в группе, добавив его публичный ключ в список забаненных публичных ключей группы + * @param groupId ID группы, в которой нужно забанить участника + * @param memberPublicKey публичный ключ участника, которого нужно забанить в группе + */ + public void banMemberInGroup(String groupId, String memberPublicKey) { + Group group = this.findByField("groupId", groupId); + if(group != null) { + List bannedPublicKeys = group.getBannedPublicKeys(); + List membersPublicKeys = group.getMembersPublicKeys(); + if(membersPublicKeys.contains(memberPublicKey)) { + membersPublicKeys.remove(memberPublicKey); + group.setMembersPublicKeys(membersPublicKeys); + } + if(!bannedPublicKeys.contains(memberPublicKey)) { + bannedPublicKeys.add(memberPublicKey); + group.setBannedPublicKeys(bannedPublicKeys); + } + this.update(group); + } + } + } diff --git a/src/main/java/com/rosetta/im/executors/Executor17GroupCreate.java b/src/main/java/com/rosetta/im/executors/Executor17GroupCreate.java new file mode 100644 index 0000000..b2f4b9a --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor17GroupCreate.java @@ -0,0 +1,36 @@ +package com.rosetta.im.executors; + +import com.rosetta.im.Failures; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.repository.GroupRepository; +import com.rosetta.im.packet.Packet17GroupCreate; +import com.rosetta.im.util.RandomUtil; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.packet.PacketExecutor; + +public class Executor17GroupCreate extends PacketExecutor { + + private final GroupRepository groupRepository = new GroupRepository(); + + @Override + public void onPacketReceived(Packet17GroupCreate packet, Client client) throws Exception, ProtocolException { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){ + /** + * Клиент не авторизован, он не может создавать группы + */ + client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); + return; + } + String groupId = RandomUtil.randomString(16); + this.groupRepository.createGroup(groupId, eciAuthentificate.getPublicKey()); + /** + * Отправляем клиенту ид созданной группы + */ + packet.setGroupId(groupId); + client.send(packet); + } + +} diff --git a/src/main/java/com/rosetta/im/executors/Executor18GroupInfo.java b/src/main/java/com/rosetta/im/executors/Executor18GroupInfo.java new file mode 100644 index 0000000..33a75bd --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor18GroupInfo.java @@ -0,0 +1,58 @@ +package com.rosetta.im.executors; + +import java.util.ArrayList; + +import com.rosetta.im.Failures; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.entity.Group; +import com.rosetta.im.database.repository.GroupRepository; +import com.rosetta.im.packet.Packet18GroupInfo; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.packet.PacketExecutor; + +public class Executor18GroupInfo extends PacketExecutor { + + private final GroupRepository groupRepository = new GroupRepository(); + + @Override + public void onPacketReceived(Packet18GroupInfo packet, Client client) throws Exception, ProtocolException { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){ + /** + * Клиент не авторизован, он не может запрашивать информацию о группах + */ + client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); + return; + } + String groupId = packet.getGroupId(); + Group group = this.groupRepository.getGroup(groupId); + if(group == null || group.getMembersPublicKeys().size() <= 0) { + /** + * Если сервер возвращает пустой список участников, + * значит группы не существует, потому что + * пустая группа быть не может, так как они автоматически + * удаляются при выходе последнего участника + */ + packet.setMembersPKs(new ArrayList<>()); + client.send(packet); + return; + } + if(!group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey())){ + /** + * Клиент не является участником группы, значит его может быть + * исключили, возвращаем пустую информацию как будто группы нет. + */ + packet.setMembersPKs(new ArrayList<>()); + client.send(packet); + return; + } + /** + * Отправляем клиенту список участников группы + */ + packet.setMembersPKs(group.getMembersPublicKeys()); + client.send(packet); + } + +} diff --git a/src/main/java/com/rosetta/im/executors/Executor19GroupInviteInfo.java b/src/main/java/com/rosetta/im/executors/Executor19GroupInviteInfo.java new file mode 100644 index 0000000..3cc5e61 --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor19GroupInviteInfo.java @@ -0,0 +1,57 @@ +package com.rosetta.im.executors; + +import com.rosetta.im.Failures; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.entity.Group; +import com.rosetta.im.database.repository.GroupRepository; +import com.rosetta.im.packet.Packet19GroupInviteInfo; +import com.rosetta.im.packet.runtime.NetworkGroupStatus; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.packet.PacketExecutor; + +public class Executor19GroupInviteInfo extends PacketExecutor { + + private final GroupRepository groupRepository = new GroupRepository(); + + @Override + public void onPacketReceived(Packet19GroupInviteInfo packet, Client client) throws Exception, ProtocolException { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){ + /** + * Клиент не авторизован, он не может запрашивать информацию о приглашениях + */ + client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); + return; + } + String groupId = packet.getGroupId(); + Group group = this.groupRepository.getGroup(groupId); + if(group == null){ + /** + * Группы не существует, возвращаем клиенту статус INVALID + */ + packet.setStatus(NetworkGroupStatus.INVALID); + client.send(packet); + return; + } + if(group.getBannedPublicKeys().contains(eciAuthentificate.getPublicKey())){ + /** + * Клиент забанен в группе, возвращаем клиенту статус BANNED + */ + packet.setStatus(NetworkGroupStatus.BANNED); + client.send(packet); + return; + } + /** + * Отправляем клиенту информацию о количестве участников и статусе, является ли + * пользователь участником группы или нет + */ + int membersCount = group.getMembersPublicKeys().size(); + boolean isMember = group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey()); + packet.setMembersCount(membersCount); + packet.setStatus(isMember ? NetworkGroupStatus.JOINED : NetworkGroupStatus.NOT_JOINED); + client.send(packet); + } + +} diff --git a/src/main/java/com/rosetta/im/executors/Executor20GroupJoin.java b/src/main/java/com/rosetta/im/executors/Executor20GroupJoin.java new file mode 100644 index 0000000..06016ed --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor20GroupJoin.java @@ -0,0 +1,65 @@ +package com.rosetta.im.executors; + +import com.rosetta.im.Failures; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.entity.Group; +import com.rosetta.im.database.repository.GroupRepository; +import com.rosetta.im.packet.Packet20GroupJoin; +import com.rosetta.im.packet.runtime.NetworkGroupStatus; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.packet.PacketExecutor; + +public class Executor20GroupJoin extends PacketExecutor { + + private final GroupRepository groupRepository = new GroupRepository(); + + @Override + public void onPacketReceived(Packet20GroupJoin packet, Client client) throws Exception, ProtocolException { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){ + /** + * Клиент не авторизован, он не может вступать в группы + */ + client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); + return; + } + String groupId = packet.getGroupId(); + Group group = this.groupRepository.getGroup(groupId); + if(group == null){ + /** + * Группы не существует, возвращаем клиенту статус INVALID + */ + packet.setStatus(NetworkGroupStatus.INVALID); + client.send(packet); + return; + } + + if(group.getBannedPublicKeys().contains(eciAuthentificate.getPublicKey())){ + /** + * Клиент забанен в группе, возвращаем клиенту статус BANNED + */ + packet.setStatus(NetworkGroupStatus.BANNED); + client.send(packet); + return; + } + + if(group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey())){ + /** + * Клиент уже является участником группы, возвращаем клиенту статус JOINED + */ + packet.setStatus(NetworkGroupStatus.JOINED); + client.send(packet); + return; + } + + /** + * Добавляем клиента в группу и возвращаем клиенту статус JOINED + */ + this.groupRepository.addMemberToGroup(groupId, eciAuthentificate.getPublicKey()); + packet.setStatus(NetworkGroupStatus.JOINED); + client.send(packet); + } + +} diff --git a/src/main/java/com/rosetta/im/executors/Executor21GroupLeave.java b/src/main/java/com/rosetta/im/executors/Executor21GroupLeave.java new file mode 100644 index 0000000..3723ad6 --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor21GroupLeave.java @@ -0,0 +1,66 @@ +package com.rosetta.im.executors; + +import com.rosetta.im.Failures; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.entity.Group; +import com.rosetta.im.database.repository.GroupRepository; +import com.rosetta.im.packet.Packet21GroupLeave; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.packet.PacketExecutor; + +/** + * Обработчик пакета выхода из группы + * Отправляет клиенту в ответ такой же пакет, если он успешно покинул группу или не состоял в ней изначально + * чтобы клиентское приложение могло корректно обновить интерфейс, например, удалить группу из списка групп пользователя + * Если клиент является единственным участником группы, то при выходе группа удаляется целиком + */ +public class Executor21GroupLeave extends PacketExecutor { + + private final GroupRepository groupRepository = new GroupRepository(); + + @Override + public void onPacketReceived(Packet21GroupLeave packet, Client client) throws Exception, ProtocolException { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){ + /** + * Клиент не авторизован, он не может покидать группы + */ + client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); + return; + } + String groupId = packet.getGroupId(); + Group group = this.groupRepository.getGroup(groupId); + if(group == null){ + /** + * Группы не существует, просто возвращаем клиенту тот же пакет, + * как будто мы успешно покинули группу, потому что по факту мы уже не состоим в ней + */ + client.send(packet); + return; + } + if(!group.getMembersPublicKeys().contains(eciAuthentificate.getPublicKey())){ + /** + * Клиент не является участником группы, просто возвращаем клиенту тот же пакет, + * как будто мы успешно покинули группу, потому что по факту мы уже не состоим в ней + */ + client.send(packet); + return; + } + if(group.getMembersPublicKeys().size() <= 1){ + /** + * Клиент является единственным участником группы, удаляем группу целиком + */ + this.groupRepository.removeGroup(groupId); + client.send(packet); + return; + } + /** + * Удаляем клиента из группы + */ + this.groupRepository.removeMemberFromGroup(groupId, eciAuthentificate.getPublicKey()); + client.send(packet); + } + +} diff --git a/src/main/java/com/rosetta/im/executors/Executor22GroupBan.java b/src/main/java/com/rosetta/im/executors/Executor22GroupBan.java new file mode 100644 index 0000000..df57748 --- /dev/null +++ b/src/main/java/com/rosetta/im/executors/Executor22GroupBan.java @@ -0,0 +1,78 @@ +package com.rosetta.im.executors; + +import java.util.List; + +import com.rosetta.im.Failures; +import com.rosetta.im.client.tags.ECIAuthentificate; +import com.rosetta.im.database.entity.Group; +import com.rosetta.im.database.repository.GroupRepository; +import com.rosetta.im.packet.Packet18GroupInfo; +import com.rosetta.im.packet.Packet22GroupBan; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.packet.PacketExecutor; + +public class Executor22GroupBan extends PacketExecutor { + + private final GroupRepository groupRepository = new GroupRepository(); + + @Override + public void onPacketReceived(Packet22GroupBan packet, Client client) throws Exception, ProtocolException { + ECIAuthentificate eciAuthentificate = client.getTag(ECIAuthentificate.class); + if(eciAuthentificate == null || !eciAuthentificate.hasAuthorized()){ + /** + * Клиент не авторизован, он не может банить участников групп + */ + client.disconnect(Failures.HANDSHAKE_NOT_COMPLETED); + return; + } + String groupId = packet.getGroupId(); + String publicKeyToBan = packet.getPublicKey(); + Group group = this.groupRepository.getGroup(groupId); + if(group == null){ + /** + * Группы не существует, но так как клиент вызывает бан участника, предполагается что он админ, + * а значит точно должен знать что группы не существует, значит это какое-то атипичное поведение + */ + client.disconnect(Failures.DATA_MISSMATCH); + return; + } + if(!group.getMembersPublicKeys().get(0).equals(eciAuthentificate.getPublicKey())){ + /** + * Администратор группы - первый участник в списке участников, + * если публичный ключ клиента не совпадает с публичным ключом первого участника, + * значит он не админ и не может банить участников + */ + client.disconnect(Failures.DATA_MISSMATCH); + return; + } + if(!group.getMembersPublicKeys().contains(publicKeyToBan)){ + /** + * Пользователя которого пытаются забанить нет в группе + */ + return; + } + /** + * Баним пользователя в группе - удаляем его из участников и добавляем в бан + */ + this.groupRepository.banMemberInGroup(groupId, publicKeyToBan); + /** + * Удаляем пользователя из списка участников (сверху мы уже удаляем его в базе, + * а здесь просто удаляем из объекта, чтобы отправить + * клиенту обновленную информацию о группе) + */ + List membersPKs = group.getMembersPublicKeys(); + membersPKs.remove(publicKeyToBan); + group.setBannedPublicKeys(membersPKs); + /** + * Отправляем клиенту новый Packet18GroupInfo, чтобы он обновил информацию о группе, + * например, удалил участника из списка участников + */ + Packet18GroupInfo groupInfoPacket = new Packet18GroupInfo(); + groupInfoPacket.setGroupId(groupId); + groupInfoPacket.setMembersPKs(group.getMembersPublicKeys()); + client.send(groupInfoPacket); + } + +} diff --git a/src/main/java/com/rosetta/im/packet/Packet17CreateGroup.java b/src/main/java/com/rosetta/im/packet/Packet17GroupCreate.java similarity index 94% rename from src/main/java/com/rosetta/im/packet/Packet17CreateGroup.java rename to src/main/java/com/rosetta/im/packet/Packet17GroupCreate.java index 01b009e..de88c11 100644 --- a/src/main/java/com/rosetta/im/packet/Packet17CreateGroup.java +++ b/src/main/java/com/rosetta/im/packet/Packet17GroupCreate.java @@ -3,7 +3,7 @@ package com.rosetta.im.packet; import io.orprotocol.Stream; import io.orprotocol.packet.Packet; -public class Packet17CreateGroup extends Packet { +public class Packet17GroupCreate extends Packet { private String groupId; diff --git a/src/main/java/com/rosetta/im/util/RandomUtil.java b/src/main/java/com/rosetta/im/util/RandomUtil.java index b1d8764..f3ca1fa 100644 --- a/src/main/java/com/rosetta/im/util/RandomUtil.java +++ b/src/main/java/com/rosetta/im/util/RandomUtil.java @@ -15,4 +15,19 @@ public class RandomUtil { return random.nextInt((max - min) + 1) + min; } + /** + * Генерирует случайную строку заданной длины, состоящую из букв и цифр + * @param length длина строки + * @return случайная строка заданной длины, состоящая из букв и цифр + */ + public static String randomString(int length) { + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder result = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < length; i++) { + result.append(characters.charAt(random.nextInt(characters.length()))); + } + return result.toString(); + } + }