From da4d1a39daec780174240002d9f0e5fc7a16faf4 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 4 Jul 2025 17:36:56 +0330 Subject: [PATCH] RealTime Action (Server) --- .../Client/IncomingMessageListener.java | 21 +- .../Database/ChannelDatabase.java | 15 + .../Database/ContactDatabase.java | 9 + .../Database/GroupDatabase.java | 15 + .../Server/ClientHandler.java | 283 ++++++++++++++++-- .../Server/RealTimeEventDispatcher.java | 46 ++- .../to/telegramfinalproject/UI/LoginForm.java | 2 +- 7 files changed, 369 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 30ca3a0..832fe3e 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -1,6 +1,5 @@ package org.to.telegramfinalproject.Client; - import org.json.JSONObject; import java.io.BufferedReader; @@ -46,7 +45,9 @@ public class IncomingMessageListener implements Runnable { case "new_message", "message_edited", "message_deleted", "user_status_changed", "added_to_group", "added_to_channel", "update_group_or_channel", "chat_deleted", - "blocked_by_user", "unblocked_by_user", "message_seen" -> true; + "blocked_by_user", "unblocked_by_user", "message_seen", + "removed_from_group", "removed_from_channel", + "became_admin", "removed_admin" -> true; default -> false; }; } @@ -111,6 +112,22 @@ public class IncomingMessageListener implements Runnable { System.out.println("Seen at: " + msg.getString("seen_at")); } + case "removed_from_group" -> { + System.out.println("\n🚫 You were removed from group: " + msg.getString("chat_id")); + } + + case "removed_from_channel" -> { + System.out.println("\n🚫 You were removed from channel: " + msg.getString("chat_id")); + } + + case "became_admin" -> { + System.out.println("\n⭐ You are now an admin in: " + msg.getString("chat_name")); + } + + case "removed_admin" -> { + System.out.println("\nāš ļø You are no longer an admin in: " + msg.getString("chat_name")); + } + default -> { System.out.println("\nā“ Unknown real-time action: " + action); } diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java index efa8535..45104e4 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java @@ -601,4 +601,19 @@ public class ChannelDatabase { } } + public static List getChannelSubscriberUUIDs(UUID channelId) { + List subscriberIds = new ArrayList<>(); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement("SELECT user_id FROM channel_subscribers WHERE channel_id = ?")) { + stmt.setObject(1, channelId); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + subscriberIds.add(UUID.fromString(rs.getString("user_id"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return subscriberIds; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java index 220f551..edcd83c 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java @@ -34,6 +34,15 @@ public class ContactDatabase { } } + public static List getContactUUIDs(UUID userId) { + List contacts = getContacts(userId); + List contactIds = new ArrayList<>(); + for (Contact c : contacts) { + contactIds.add(c.getContact_id()); + } + return contactIds; + } + public boolean removeContact(UUID user_id, UUID contact_id) { diff --git a/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java index 008a147..aedde59 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java @@ -600,4 +600,19 @@ public class GroupDatabase { } + public static List getGroupMemberUUIDs(UUID groupId) { + List memberIds = new ArrayList<>(); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement("SELECT user_id FROM group_members WHERE group_id = ?")) { + stmt.setObject(1, groupId); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + memberIds.add(UUID.fromString(rs.getString("user_id"))); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return memberIds; + } + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 76022b6..6cf26ef 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -79,6 +79,10 @@ public class ClientHandler implements Runnable { SessionManager.addUser(user.getInternal_uuid(), this.socket); userDatabase.updateUserStatus(user.getInternal_uuid(), "online"); + //RealTime + List contactIds = ContactDatabase.getContactUUIDs(user.getInternal_uuid()); + RealTimeEventDispatcher.notifyUserStatusChanged(user.getInternal_uuid(), "online", contactIds); + List contacts = ContactDatabase.getContacts(user.getInternal_uuid()); List groups = GroupDatabase.getGroupsByUser(user.getInternal_uuid()); List channels = ChannelDatabase.getChannelsByUser(user.getInternal_uuid()); @@ -158,10 +162,17 @@ public class ClientHandler implements Runnable { String user_Id = requestJson.optString("user_id"); if (user_Id != null && !user_Id.isEmpty()) { try { - UUID uuid = UUID.fromString(user_Id); - userDatabase.updateUserStatus(uuid, "offline"); - userDatabase.updateLastSeen(uuid); - SessionManager.removeUser(uuid); + userId = UUID.fromString(user_Id); + + userDatabase.updateUserStatus(userId, "offline"); + + // Real-Time + List contacts = ContactDatabase.getContactUUIDs(userId); + RealTimeEventDispatcher.notifyUserStatusChanged(userId, "offline", contacts); + + userDatabase.updateLastSeen(userId); + SessionManager.removeUser(userId); + response = new ResponseModel("success", "Logged out."); } catch (IllegalArgumentException e) { response = new ResponseModel("error", "Invalid UUID format."); @@ -320,6 +331,18 @@ public class ClientHandler implements Runnable { UUID contactUUID = UUID.fromString(requestJson.getString("contact_id")); boolean success = ContactDatabase.addContact(userUUID, contactUUID); + + //RealTime + if (success) { + RealTimeEventDispatcher.notifyAddedToChat( + "private", + userUUID, + currentUser.getProfile_name(), + currentUser.getImage_url(), + contactUUID + ); + } + response = success ? new ResponseModel("success", "Contact added successfully.") : new ResponseModel("error", "Failed to add contact. Maybe already exists."); @@ -336,6 +359,13 @@ public class ClientHandler implements Runnable { boolean joined = GroupDatabase.addMemberToGroup(userUUID, group.getInternal_uuid()); + + //RealTime + if (joined) { + List members = GroupDatabase.getMemberUUIDs(group.getInternal_uuid()); + RealTimeEventDispatcher.sendGroupOrChannelUpdate("group", group.getInternal_uuid(), group.getGroup_name(), group.getImage_url(), group.getDescription(), members); + } + response = joined ? new ResponseModel("success", "Joined group.") : new ResponseModel("error", "Failed to join group."); @@ -352,6 +382,13 @@ public class ClientHandler implements Runnable { boolean joined = ChannelDatabase.addSubscriberToChannel(userUUID, channel.getInternal_uuid()); + + //RealTime + if (joined) { + List members = ChannelDatabase.getChannelSubscriberUUIDs(channel.getInternal_uuid()); + RealTimeEventDispatcher.sendGroupOrChannelUpdate("channel", channel.getInternal_uuid(), channel.getChannel_name(), channel.getImage_url(), channel.getDescription(), members); + } + response = joined ? new ResponseModel("success", "Joined channel.") : new ResponseModel("error", "Failed to join channel."); @@ -620,6 +657,20 @@ public class ClientHandler implements Runnable { } boolean success = ChannelDatabase.addAdminToChannel(channelId, targetUserId, permissions); + + //RealTime + if (success) { + Channel channel = ChannelDatabase.findByInternalUUID(channelId); + if (channel != null) { + RealTimeEventDispatcher.notifyBecameAdmin( + "channel", + channel.getInternal_uuid(), + channel.getChannel_name(), + channel.getImage_url(), + targetUserId + ); + } + } response = success ? new ResponseModel("success", "Admin added to channel.") : new ResponseModel("error", "Failed to add admin."); @@ -673,9 +724,11 @@ public class ClientHandler implements Runnable { ? new ResponseModel("success", "Group info updated successfully.") : new ResponseModel("error", "Failed to update group info."); - //if (updated) { - //RealTimeEventDispatcher.sendGroupOrChannelUpdate(groupUUID, "group", name); - //} + //RealTime + if (updated) { + List members = GroupDatabase.getMemberUUIDs(groupUUID); + RealTimeEventDispatcher.sendGroupOrChannelUpdate("group", groupUUID, name, imageUrl, description, members); + } } catch (Exception e) { response = new ResponseModel("error", "Error updating group: " + e.getMessage()); @@ -689,10 +742,6 @@ public class ClientHandler implements Runnable { case "view_channel_admins": { UUID channelId = UUID.fromString(requestJson.getString("channel_id")); - //if (!ChannelPermissionUtil.canAddAdmins(channelId, currentUser.getInternal_uuid())) { - // response = new ResponseModel("error", "You are not allowed to add admins to the channel."); - //break; - //} List admins = ChannelDatabase.getChannelAdminsAndOwner(channelId); @@ -716,6 +765,20 @@ public class ClientHandler implements Runnable { boolean success = GroupDatabase.addAdminToGroup(groupId, targetUserId, permissions); + + //RealTime + if (success) { + Group group = GroupDatabase.findByInternalUUID(groupId); + if (group != null) { + RealTimeEventDispatcher.notifyBecameAdmin( + "group", + group.getInternal_uuid(), + group.getGroup_name(), + group.getImage_url(), + targetUserId + ); + } + } response = success ? new ResponseModel("success", "Admin added to group.") : new ResponseModel("error", "Failed to add admin."); @@ -764,6 +827,21 @@ public class ClientHandler implements Runnable { } boolean success = ChannelDatabase.demoteAdminToSubscriber(channelId, targetUserUUID); + + //RealTime + if (success) { + Channel channel = ChannelDatabase.findByInternalUUID(channelId); + if (channel != null) { + RealTimeEventDispatcher.notifyRemovedAdminFromChat( + "channel", + channel.getInternal_uuid(), + channel.getChannel_name(), + channel.getImage_url(), + targetUserUUID + ); + } + } + response = success ? new ResponseModel("success", "Admin removed successfully.") : new ResponseModel("error", "Failed to remove admin."); @@ -786,10 +864,25 @@ public class ClientHandler implements Runnable { } boolean success = GroupDatabase.addMemberToGroup(targetUserId, groupId); + + Group group = GroupDatabase.findByInternalUUID(groupId); + + //RealTime + if (success && group != null) { + RealTimeEventDispatcher.notifyAddedToChat( + "group", + group.getInternal_uuid(), + group.getGroup_name(), + group.getImage_url(), + targetUserId + ); + } + response = success ? new ResponseModel("success", "Member added to group.") : new ResponseModel("error", "Failed to add member."); break; + } @@ -813,7 +906,18 @@ public class ClientHandler implements Runnable { break; } + //RealTime boolean success = GroupDatabase.removeMemberFromGroup(groupId, targetUserId); + if (success) { + Group group = GroupDatabase.findByInternalUUID(groupId); + if (group != null) { + RealTimeEventDispatcher.notifyRemovedFromChat( + "group", + group.getInternal_uuid(), + targetUserId + ); + } + } response = success ? new ResponseModel("success", "Member removed from group.") : new ResponseModel("error", "Failed to remove member."); @@ -911,6 +1015,14 @@ public class ClientHandler implements Runnable { boolean isBlocked = ContactDatabase.toggleBlock(userUUID, targetUUID); + //RealTime + if (isBlocked) { + RealTimeEventDispatcher.notifyBlocked(userUUID, targetUUID); + } else { + RealTimeEventDispatcher.notifyUnblocked(userUUID, targetUUID); + } + + String message = isBlocked ? "šŸ”’ User blocked successfully." : "šŸ”“ User unblocked successfully."; response = new ResponseModel("success", message); @@ -944,6 +1056,13 @@ public class ClientHandler implements Runnable { boolean success = GroupDatabase.transferOwnership(groupId, newOwner.getInternal_uuid()); + //RealTime + if (success) { + List members = GroupDatabase.getMemberUUIDs(groupId); + RealTimeEventDispatcher.sendGroupOrChannelUpdate("group", groupId, groupId.toString(), "", "", members); + } + + response = success ? new ResponseModel("success", "Ownership transferred.") : new ResponseModel("error", "Failed to transfer ownership."); @@ -972,6 +1091,14 @@ public class ClientHandler implements Runnable { case "delete_private_chat" : { UUID targetId = UUID.fromString(requestJson.getString("target_id")); boolean both = requestJson.getBoolean("both"); + + //RealTime + if (both) { + RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); + RealTimeEventDispatcher.notifyChatDeleted("private", currentUser.getInternal_uuid(), List.of(targetId)); + } else { + RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); + } response = PrivateChatService.deletePrivateChat(currentUser.getInternal_uuid(), targetId, both); break; } @@ -995,6 +1122,9 @@ public class ClientHandler implements Runnable { boolean success = false; + + + switch (chatType) { case "group" -> success = GroupDatabase.removeMemberFromGroup(chatId, userId); case "channel" -> success = ChannelDatabase.removeSubscriberFromChannel(chatId, userId); @@ -1004,6 +1134,15 @@ public class ClientHandler implements Runnable { } } + //RealTime + if (success) { + if (chatType.equals("group")) { + RealTimeEventDispatcher.notifyRemovedFromChat("group", chatId, userId); + } else if (chatType.equals("channel")) { + RealTimeEventDispatcher.notifyRemovedFromChat("channel", chatId, userId); + } + } + if (response == null) { response = success ? new ResponseModel("success", "Left the " + chatType + " successfully.") @@ -1022,11 +1161,17 @@ public class ClientHandler implements Runnable { break; } + List memberIds = GroupDatabase.getGroupMemberUUIDs(groupId); + boolean success = GroupDatabase.deleteGroup(groupId); - response = success - ? new ResponseModel("success", "Group deleted successfully.") - : new ResponseModel("error", "Failed to delete group."); + //RealTime + if (success) { + RealTimeEventDispatcher.notifyChatDeleted("group", groupId, memberIds); + response = new ResponseModel("success", "Group deleted successfully."); + } else { + response = new ResponseModel("error", "Failed to delete group."); + } break; } @@ -1085,6 +1230,20 @@ public class ClientHandler implements Runnable { } boolean success = ChannelDatabase.addSubscriberToChannel(targetUserId, channelId); + + //RealTime + if (success) { + Channel channel = ChannelDatabase.findByInternalUUID(channelId); + if (channel != null) { + RealTimeEventDispatcher.notifyAddedToChat( + "channel", + channel.getInternal_uuid(), + channel.getChannel_name(), + channel.getImage_url(), + targetUserId + ); + } + } response = success ? new ResponseModel("success", "Subscriber added to channel.") : new ResponseModel("error", "Failed to add subscriber."); @@ -1112,6 +1271,18 @@ public class ClientHandler implements Runnable { } boolean success = ChannelDatabase.removeSubscriberFromChannel(channelId, targetUserId); + + //RealTime + if (success) { + Channel channel = ChannelDatabase.findByInternalUUID(channelId); + if (channel != null) { + RealTimeEventDispatcher.notifyRemovedFromChat( + "channel", + channel.getInternal_uuid(), + targetUserId + ); + } + } response = success ? new ResponseModel("success", "Subscriber removed from channel.") : new ResponseModel("error", "Failed to remove subscriber."); @@ -1140,6 +1311,13 @@ public class ClientHandler implements Runnable { } boolean updated = ChannelDatabase.updateChannelInfo(channelUUID, newChannelId, name, description, imageUrl); + + //RealTime + if (updated) { + List subscribers = ChannelDatabase.getChannelSubscriberUUIDs(channelUUID); + RealTimeEventDispatcher.sendGroupOrChannelUpdate("channel", channelUUID, name, imageUrl, description, subscribers); + } + response = updated ? new ResponseModel("success", "Channel info updated successfully.") : new ResponseModel("error", "Failed to update channel info."); @@ -1159,14 +1337,70 @@ public class ClientHandler implements Runnable { break; } + List subscriberIds = ChannelDatabase.getChannelSubscriberUUIDs(channelId); + boolean success = ChannelDatabase.deleteChannel(channelId); - response = success - ? new ResponseModel("success", "Channel deleted successfully.") - : new ResponseModel("error", "Failed to delete channel."); + + //RealTime + if (success) { + RealTimeEventDispatcher.notifyChatDeleted("channel", channelId, subscriberIds); + response = new ResponseModel("success", "Channel deleted successfully."); + } else { + response = new ResponseModel("error", "Failed to delete channel."); + } break; } + case "remove_admin_from_group": { + UUID groupId = UUID.fromString(requestJson.getString("group_id")); + UUID targetUserId = UUID.fromString(requestJson.getString("target_user_id")); + + if (!GroupPermissionUtil.canRemoveAdmins(groupId, currentUser.getInternal_uuid())) { + response = new ResponseModel("error", "You are not allowed to remove admins."); + break; + } + + String targetRole = GroupDatabase.getGroupRole(groupId, targetUserId); + if (targetRole == null) { + response = new ResponseModel("error", "User is not a member of the group."); + break; + } + + if (targetRole.equals("owner")) { + response = new ResponseModel("error", "You cannot remove the owner."); + break; + } + + if (!targetRole.equals("admin")) { + response = new ResponseModel("error", "Target user is not an admin."); + break; + } + + boolean success = GroupDatabase.demoteAdminToMember(groupId, targetUserId); + + // RealTime + if (success) { + Group group = GroupDatabase.findByInternalUUID(groupId); + if (group != null) { + RealTimeEventDispatcher.notifyRemovedAdminFromChat( + "group", + group.getInternal_uuid(), + group.getGroup_name(), + group.getImage_url(), + targetUserId + ); + } + } + + response = success + ? new ResponseModel("success", "Admin removed successfully.") + : new ResponseModel("error", "Failed to remove admin."); + + break; + } + + case "transfer_channel_ownership": { UUID channelId = UUID.fromString(requestJson.getString("channel_id")); String newOwnerUserIdStr = requestJson.getString("new_owner_user_id"); @@ -1183,7 +1417,15 @@ public class ClientHandler implements Runnable { } boolean success = ChannelDatabase.transferOwnership(channelId, newOwner.getInternal_uuid()); + + //RealTime + if (success) { + List subscribers = ChannelDatabase.getChannelSubscriberUUIDs(channelId); + RealTimeEventDispatcher.sendGroupOrChannelUpdate("channel", channelId, channelId.toString(), "", "", subscribers); + } + response = success + ? new ResponseModel("success", "Ownership transferred successfully.") : new ResponseModel("error", "Failed to transfer ownership."); break; @@ -1212,12 +1454,17 @@ public class ClientHandler implements Runnable { } finally { try { if (currentUser != null) { + //RealTime userId = currentUser.getInternal_uuid(); + List contacts = ContactDatabase.getContactUUIDs(userId); + RealTimeEventDispatcher.notifyUserStatusChanged(userId, "offline", contacts); + System.out.println("šŸ”š Client disconnected. Cleaning up user " + userId); userDatabase.updateUserStatus(userId, "offline"); userDatabase.updateLastSeen(userId); SessionManager.removeUser(userId); - } else { + } + else { System.out.println("ā— currentUser is null, couldn't set offline."); } socket.close(); diff --git a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java index 68e89ba..be934b2 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -176,7 +176,7 @@ public class RealTimeEventDispatcher { public static void notifyAddedToChat(String type, UUID chatId, String chatName, String imageUrl, UUID userId) { JSONObject data = new JSONObject(); - data.put("chat_type", type); // group یا channel + data.put("chat_type", type); data.put("chat_id", chatId.toString()); data.put("chat_name", chatName); data.put("image_url", imageUrl); @@ -246,5 +246,49 @@ public class RealTimeEventDispatcher { broadcastToUsers(contacts, event); } + public static void sendGroupOrChannelUpdate(String type, UUID chatId, String name, String imageUrl, String description, List affectedUsers) { + JSONObject data = new JSONObject(); + data.put("chat_type", type); + data.put("chat_id", chatId.toString()); + data.put("name", name); + data.put("image_url", imageUrl); + data.put("description", description != null ? description : ""); + + JSONObject event = new JSONObject(); + event.put("action", "chat_updated"); + event.put("data", data); + + broadcastToUsers(affectedUsers, event); + } + + + public static void notifyBecameAdmin(String type, UUID chatId, String chatName, String imageUrl, UUID userId) { + JSONObject data = new JSONObject(); + data.put("chat_type", type); // group یا channel + data.put("chat_id", chatId.toString()); + data.put("chat_name", chatName); + data.put("image_url", imageUrl); + + JSONObject event = new JSONObject(); + event.put("action", "became_admin"); + event.put("data", data); + + sendToUser(userId, event); + } + + + public static void notifyRemovedAdminFromChat(String type, UUID chatId, String chatName, String imageUrl, UUID userId) { + JSONObject data = new JSONObject(); + data.put("chat_type", type); + data.put("chat_id", chatId.toString()); + data.put("chat_name", chatName); + data.put("image_url", imageUrl); + + JSONObject event = new JSONObject(); + event.put("action", "removed_admin"); + event.put("data", data); + + sendToUser(userId, event); + } } diff --git a/src/main/java/org/to/telegramfinalproject/UI/LoginForm.java b/src/main/java/org/to/telegramfinalproject/UI/LoginForm.java index 79ae18e..2b6b929 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginForm.java +++ b/src/main/java/org/to/telegramfinalproject/UI/LoginForm.java @@ -33,7 +33,7 @@ public class LoginForm { public void initialize() { try { - connection = new ClientConnection("localhost", 12345); + connection = new ClientConnection("localhost", 8000); } catch (Exception e) { System.out.println("Could not connect to server: " + e.getMessage()); }