From 2c19db1d42d1336d41593cb46b46cccad86756d8 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 7 Aug 2025 13:03:01 +0330 Subject: [PATCH] Delete messages in chats --- .../Client/ActionHandler.java | 183 +++++++++++++++++- .../Database/MessageDatabase.java | 111 +++++++++++ .../telegramfinalproject/Models/Message.java | 19 ++ .../Server/ClientHandler.java | 106 ++++++++++ .../Utils/ChannelPermissionUtil.java | 22 +++ 5 files changed, 438 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 36ab8f5..df6ca8c 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1242,7 +1242,8 @@ public class ActionHandler { System.out.println("4. Delete chat (both sides)"); System.out.println("5. View profile"); System.out.println("6. Archive/Unarchived"); - System.out.println("7. Back"); + System.out.println("7. View messages"); + System.out.println("8. Back"); String input = scanner.nextLine(); @@ -1288,8 +1289,11 @@ public class ActionHandler { return true; } + case "7" ->{ + viewMessagesInChat(chat); + } - case "7" -> { + case "8" -> { return false; } default -> System.out.println("Invalid choice."); @@ -1355,6 +1359,7 @@ public class ActionHandler { } System.out.println("12. Archive/Unarchived"); + System.out.println("13. View messages"); System.out.println("0. Back to Chat List"); @@ -1409,6 +1414,10 @@ public class ActionHandler { toggleArchive(chat.getId() , "group"); } + case "13" ->{ + viewMessagesInChat(chat); + } + case "0" -> { return false; } @@ -1476,6 +1485,7 @@ public class ActionHandler { } System.out.println("12. Archive/Unarchived"); + System.out.println("13. View messages"); System.out.println("0. Back to Chat List"); String input = scanner.nextLine(); @@ -1558,7 +1568,9 @@ public class ActionHandler { case "12"->{ toggleArchive(chat.getId() , "channel"); } - + case "13" ->{ + viewMessagesInChat(chat); + } case "0" -> { return false; @@ -3327,8 +3339,173 @@ public class ActionHandler { } } + private void viewMessagesInChat(ChatEntry chat) { + int offset = 0; + int limit = 10; + while (true) { + JSONObject req = new JSONObject(); + req.put("action", "get_chat_messages"); + req.put("chat_id", chat.getId()); + req.put("chat_type", chat.getType()); + req.put("offset", offset); + req.put("limit", limit); + JSONObject res = sendWithResponse(req); + if (res == null || !res.getString("status").equals("success")) { + System.out.println("āŒ Failed to fetch messages."); + return; + } + + JSONArray messages = res.getJSONObject("data").getJSONArray("get_chat_messages"); + + if (messages.isEmpty()) { + if (offset == 0) + System.out.println("šŸ“­ No messages in this chat."); + else + System.out.println("šŸ“­ No more messages."); + return; + } + + System.out.println("\nšŸ“„ Messages:"); + System.out.println("─────────────────────────────────────────────"); + for (int i = 0; i < messages.length(); i++) { + JSONObject msg = messages.getJSONObject(i); + String senderName = msg.optString("sender_name", "Unknown"); + String content = msg.optString("content", "(no content)"); + String time = msg.optString("time", ""); + boolean isEdited = msg.optBoolean("is_edited", false); + String label = isEdited ? "šŸ–Šļø (edited)" : ""; + System.out.printf("[%d] [%s] %s: %s %s\n", i + 1, time, senderName, content, label); + } + System.out.println("─────────────────────────────────────────────"); + + System.out.print(""" + šŸ’¬ Options: + [number] - Interact with message + N - Next page (older messages) + 0 - Back to chat menu + āž¤ Choice: """); + + String input = scanner.nextLine().trim(); + + if (input.equals("0")) return; + if (input.equalsIgnoreCase("N")) { + offset += limit; + continue; + } + + try { + int index = Integer.parseInt(input); + if (index < 1 || index > messages.length()) { + System.out.println("āŒ Invalid message number."); + continue; + } + + JSONObject selected = messages.getJSONObject(index - 1); + UUID messageId = UUID.fromString(selected.getString("message_id")); + UUID senderId = UUID.fromString(selected.getString("sender_id")); + + System.out.println("\nšŸŽÆ Selected message by " + selected.getString("sender_name")); + System.out.println("1. Reply"); + System.out.println("2. Forward"); + System.out.println("3. React"); + + if (senderId.toString().equals(Session.currentUser.getString("internal_uuid"))) { + System.out.println("4. Edit"); + System.out.println("5. Delete"); + } + + System.out.println("0. Back to message list"); + + String choice = scanner.nextLine().trim(); + switch (choice) { + case "1" -> replyToMessage(messageId); + case "2" -> forwardMessage(messageId); + case "3" -> reactToMessage(messageId); + case "4" -> { + if (senderId.toString().equals(Session.currentUser.getString("internal_uuid"))) + editMessage(messageId); + } + case "5" -> { + if (senderId.toString().equals(Session.currentUser.getString("internal_uuid"))) + deleteMessage(messageId); + } + case "0" -> {} + default -> System.out.println("āŒ Invalid option."); + } + + } catch (NumberFormatException e) { + System.out.println("āŒ Please enter a valid number or command."); + } + } + } + + private String editMessage(UUID messageId) { + return "not ready"; + + } + + private String reactToMessage(UUID messageId) { + return "not ready"; + + } + + private String forwardMessage(UUID messageId) { + return "not ready"; + } + + private String replyToMessage(UUID messageId) { + return "not ready"; + } + + private void deleteMessage(UUID messageId) { + System.out.println("\nšŸ—‘ļø Delete Message Options:"); + System.out.println("1. Delete for yourself (one-sided)"); + System.out.println("2. Delete for everyone (global) [only if allowed]"); + System.out.println("0. Cancel"); + + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> { + JSONObject req = new JSONObject(); + req.put("action", "delete_message"); + req.put("message_id", messageId.toString()); + req.put("delete_type", "one-sided"); + + JSONObject res = sendWithResponse(req); + if (res != null && res.getString("status").equals("success")) { + System.out.println("āœ… Message deleted for you."); + } else { + System.out.println("āŒ Failed to delete message."); + } + } + + case "2" -> { + JSONObject req = new JSONObject(); + req.put("action", "delete_message"); + req.put("message_id", messageId.toString()); + req.put("delete_type", "global"); + + JSONObject res = sendWithResponse(req); + if (res != null && res.getString("status").equals("success")) { + System.out.println("āœ… Message deleted for everyone."); + } else { + System.out.println("āŒ Failed to delete message globally."); + if (res != null) System.out.println("āš ļø " + res.optString("message")); + } + } + + case "0" -> { + System.out.println("āŽ Delete canceled."); + } + + default -> { + System.out.println("āŒ Invalid option."); + } + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 345bef5..ec185e9 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -111,7 +111,116 @@ public class MessageDatabase { } } + public static List getMessagesForChat(UUID chatId, String chatType, UUID currentUserId, int offset, int limit) { + List messages = new ArrayList<>(); + String sql = """ + SELECT * FROM messages m + WHERE m.receiver_type = ? + AND m.receiver_id = ? + AND m.is_deleted_globally = FALSE + AND NOT EXISTS ( + SELECT 1 FROM deleted_messages d + WHERE d.message_id = m.message_id + AND d.user_id = ? + ) + ORDER BY m.send_at DESC + LIMIT ? OFFSET ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, chatType); + ps.setObject(2, chatId); + ps.setObject(3, currentUserId); + ps.setInt(4, limit); + ps.setInt(5, offset); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + Message msg = new Message( + UUID.fromString(rs.getString("message_id")), + UUID.fromString(rs.getString("sender_id")), + rs.getString("receiver_type"), + UUID.fromString(rs.getString("receiver_id")), + rs.getString("content"), + rs.getString("message_type"), + rs.getTimestamp("send_at").toLocalDateTime(), + rs.getString("status"), + rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, + rs.getBoolean("is_edited"), + rs.getBoolean("is_deleted_globally"), + rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, + rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, + rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null + ); + + messages.add(msg); + } + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return messages; + } + + public static Message findById(UUID messageId) { + String sql = "SELECT * FROM messages WHERE message_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return new Message( + UUID.fromString(rs.getString("message_id")), + UUID.fromString(rs.getString("sender_id")), + rs.getString("receiver_type"), + UUID.fromString(rs.getString("receiver_id")), + rs.getString("content"), + rs.getString("message_type"), + rs.getTimestamp("send_at").toLocalDateTime(), + rs.getString("status"), + rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, + rs.getBoolean("is_edited"), + rs.getBoolean("is_deleted_globally"), + rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, + rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, + rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null + ); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public static boolean markAsDeletedForUser(UUID messageId, UUID userId) { + String sql = "INSERT INTO deleted_messages (message_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, userId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean markAsGloballyDeleted(UUID messageId) { + String sql = "UPDATE messages SET is_deleted_globally = true WHERE message_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } public void markMessageAsRead(UUID messageId, UUID userId) { @@ -752,4 +861,6 @@ public class MessageDatabase { + + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index cccf880..b7dcca8 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -20,6 +20,9 @@ public class Message { private UUID forwarded_by; private UUID forwarded_from; private List attachments; + private transient String sender_name; + private transient String receiver_name; + // āœ… Full Constructor public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content, @@ -101,4 +104,20 @@ public class Message { public List getAttachments() { return attachments; } public void setAttachments(List attachments) { this.attachments = attachments; } + public String getSender_name() { + return sender_name; + } + + public void setSender_name(String sender_name) { + this.sender_name = sender_name; + } + + public String getReceiver_name() { + return receiver_name; + } + + public void setReceiver_name(String receiver_name) { + this.receiver_name = receiver_name; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 918f441..3a6e57c 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -2104,7 +2104,111 @@ public class ClientHandler implements Runnable { break; } + case "get_chat_messages" : { + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + String chatType = requestJson.getString("chat_type"); + int offset = requestJson.optInt("offset", 0); // Ł¾ŪŒŲ“ā€ŒŁŲ±Ų¶ 0 + int limit = requestJson.optInt("limit", 10); // Ł¾ŪŒŲ“ā€ŒŁŲ±Ų¶ 10 + List messages = MessageDatabase.getMessagesForChat(chatId, chatType, currentUser.getInternal_uuid(), offset, limit); + + JSONArray result = new JSONArray(); + for (Message m : messages) { + JSONObject obj = new JSONObject(); + obj.put("message_id", m.getMessage_id().toString()); + obj.put("sender_id", m.getSender_id().toString()); + + User sender = userDatabase.findByInternalUUID(m.getSender_id()); + obj.put("sender_name", sender != null ? sender.getProfile_name() : "Unknown"); + + obj.put("receiver_id", m.getReceiver_id().toString()); + obj.put("receiver_type", m.getReceiver_type()); + + String receiverName = switch (m.getReceiver_type()) { + case "group" -> { + Group g = GroupDatabase.findByInternalUUID(m.getReceiver_id()); + yield g != null ? g.getGroup_name() : "Unknown group"; + } + case "channel" -> { + Channel c = ChannelDatabase.findByInternalUUID(m.getReceiver_id()); + yield c != null ? c.getChannel_name() : "Unknown channel"; + } + case "private" -> { + UUID otherId = m.getSender_id().equals(currentUser.getInternal_uuid()) ? m.getReceiver_id() : m.getSender_id(); + User other = userDatabase.findByInternalUUID(otherId); + yield other != null ? other.getProfile_name() : "Unknown user"; + } + default -> "Unknown"; + }; + obj.put("receiver_name", receiverName); + + obj.put("content", m.getContent()); + obj.put("message_type", m.getMessage_type()); + obj.put("time", m.getSend_at().toString()); + obj.put("is_edited", m.isIs_edited()); + obj.put("is_deleted_globally", m.isIs_deleted_globally()); + + result.put(obj); + } + + JSONObject data = new JSONObject(); + data.put("get_chat_messages", result); + + response = new ResponseModel("success", "get messages ", data); + break; + } + + case "delete_message" : { + UUID messageId = UUID.fromString(requestJson.getString("message_id")); + String deleteType = requestJson.getString("delete_type"); + + Message msg = MessageDatabase.findById(messageId); + if (msg == null) { + response = new ResponseModel("error", "Message not found."); + break; + } + + UUID currentUserId = currentUser.getInternal_uuid(); + + if (deleteType.equals("one-sided")) { + boolean success = MessageDatabase.markAsDeletedForUser(messageId, currentUserId); + if (success) + response = new ResponseModel("success", "Message deleted for current user."); + else + response = new ResponseModel("error", "Failed to delete message for user."); + break; + } + + //If allowed to delete the message + if (deleteType.equals("global")) { + boolean allowed = false; + + String type = msg.getReceiver_type(); + UUID chatId = msg.getReceiver_id(); + + if (type.equals("private") || type.equals("group")) { + allowed = msg.getSender_id().equals(currentUserId); + } else if (type.equals("channel")) { + //Only owner and admins can delete messages + allowed = ChannelPermissionUtil.canDeleteMessage(chatId, currentUserId); + } + + if (!allowed) { + response = new ResponseModel("error", "You are not allowed to delete this message globally."); + break; + } + + boolean success = MessageDatabase.markAsGloballyDeleted(messageId); + if (success) + response = new ResponseModel("success", "Message deleted globally."); + else + response = new ResponseModel("error", "Failed to delete message globally."); + break; + } + + response = new ResponseModel("error", "Invalid delete_type."); + break; + } @@ -2289,4 +2393,6 @@ public class ClientHandler implements Runnable { + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java b/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java index fbdc5ad..709fee2 100644 --- a/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java @@ -2,7 +2,12 @@ package org.to.telegramfinalproject.Utils; import org.json.JSONObject; import org.to.telegramfinalproject.Database.ChannelDatabase; +import org.to.telegramfinalproject.Database.ConnectionDb; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.UUID; public class ChannelPermissionUtil { @@ -51,4 +56,21 @@ public class ChannelPermissionUtil { } return false; } + + public static boolean canDeleteMessage(UUID channelId, UUID userId) { + String roleSql = "SELECT role FROM channel_subscribers WHERE channel_id = ? AND user_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(roleSql)) { + ps.setObject(1, channelId); + ps.setObject(2, userId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String role = rs.getString("role"); + return role.equalsIgnoreCase("owner") || role.equalsIgnoreCase("admin"); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } }