diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 21303a4..3c7e815 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -268,34 +268,45 @@ public class ActionHandler { if (response.has("data") && !response.isNull("data")) { JSONObject data = response.getJSONObject("data"); - //is chat list available - if (!data.has("chat_list") || data.isNull("chat_list")) { - System.out.println("āŒ chat_list not found in response data."); + if ((!data.has("active_chat_list") || data.isNull("active_chat_list")) && + (!data.has("archived_chat_list") || data.isNull("archived_chat_list"))) { + System.out.println("āŒ No chat list found in response data."); return; } - JSONArray chatListJson = data.getJSONArray("chat_list"); - List chatList = new ArrayList<>(); + List activeChats = new ArrayList<>(); + List archivedChats = new ArrayList<>(); - for (Object obj : chatListJson) { - JSONObject chat = (JSONObject) obj; - - ChatEntry entry = new ChatEntry( - UUID.fromString(chat.getString("internal_id")), - chat.getString("id"), - chat.getString("name"), - chat.optString("image_url", ""), - chat.getString("type"), - chat.isNull("last_message_time") ? null : LocalDateTime.parse(chat.getString("last_message_time")), - chat.optBoolean("is_owner", false), - chat.optBoolean("is_admin", false) - ); - - chatList.add(entry); + // šŸ“ Parse active chat list + if (data.has("active_chat_list") && !data.isNull("active_chat_list")) { + JSONArray activeJson = data.getJSONArray("active_chat_list"); + for (Object obj : activeJson) { + JSONObject chat = (JSONObject) obj; + ChatEntry entry = parseChatEntry(chat); + activeChats.add(entry); + } } - Session.chatList = chatList; - System.out.println("āœ… Chat list updated. Total: " + chatList.size()); + // šŸ“ Parse archived chat list + if (data.has("archived_chat_list") && !data.isNull("archived_chat_list")) { + JSONArray archivedJson = data.getJSONArray("archived_chat_list"); + for (Object obj : archivedJson) { + JSONObject chat = (JSONObject) obj; + ChatEntry entry = parseChatEntry(chat); + archivedChats.add(entry); + } + } + + Session.chatList = new ArrayList<>(); + Session.chatList.addAll(activeChats); + Session.chatList.addAll(archivedChats); + + Session.activeChats = activeChats; + Session.archivedChats = archivedChats; + + System.out.println("āœ… Chat list updated."); + System.out.println("šŸ“‚ Active Chats: " + activeChats.size()); + System.out.println("šŸ“ Archived Chats: " + archivedChats.size()); } else { System.out.println("āš ļø Response has no data object."); } @@ -312,6 +323,26 @@ public class ActionHandler { } } + private ChatEntry parseChatEntry(JSONObject chat) { + ChatEntry entry = new ChatEntry( + UUID.fromString(chat.getString("internal_id")), + chat.getString("id"), + chat.getString("name"), + chat.optString("image_url", ""), + chat.getString("type"), + chat.isNull("last_message_time") ? null : LocalDateTime.parse(chat.getString("last_message_time")), + chat.optBoolean("is_owner", false), + chat.optBoolean("is_admin", false) + ); + + if (chat.has("other_user_id") && !chat.isNull("other_user_id")) { + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); + } + + return entry; + } + + public void createGroup() { String groupId = null; @@ -489,7 +520,7 @@ public class ActionHandler { chat.optBoolean("is_admin", false) ); if (chat.has("other_user_id")) { - entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // šŸ‘ˆ اضافه کردن برای private chat + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); } archivedChats.add(entry); @@ -510,7 +541,7 @@ public class ActionHandler { chat.optBoolean("is_admin", false) ); if (chat.has("other_user_id")) { - entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // šŸ‘ˆ اضافه کردن برای private chat + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); } activeChats.add(entry); @@ -576,12 +607,24 @@ public class ActionHandler { String type = item.getString("type"); if (type.equals("message")) { - System.out.println((i + 1) + ". [message] \"" + item.getString("content") + "\"" - + " (from: " + item.optString("sender", "N/A") + ", at: " + item.getString("time") + ")"); + String senderName = item.optString("sender_name", item.optString("sender", "Unknown")); + String time = item.optString("time", "Unknown time"); + String content = item.optString("content", "[No content]"); + String context = ""; + + if (item.has("group_name")) { + context = " in group: " + item.getString("group_name"); + } else if (item.has("channel_name")) { + context = " in channel: " + item.getString("channel_name"); + } + + System.out.println((i + 1) + ". [message] \"" + content + "\"" + + " (from: " + senderName + context + ", at: " + time + ")"); } else { System.out.println((i + 1) + ". [" + type + "] " + item.getString("name") + " (ID: " + item.getString("id") + ")"); } + } System.out.print("Select a result number to interact (or 0 to exit): "); @@ -683,6 +726,22 @@ public class ActionHandler { case "message" -> { String receiverId = selected.getString("receiver_id"); String receiverType = selected.getString("receiver_type"); + String senderName = selected.optString("sender_name", "Unknown"); + String content = selected.optString("content", "[No content]"); + String time = selected.optString("time", "Unknown time"); + + String context = ""; + if (receiverType.equals("group")) { + context = "in group: " + selected.optString("group_name", "Unknown group"); + } else if (receiverType.equals("channel")) { + context = "in channel: " + selected.optString("channel_name", "Unknown channel"); + } + + System.out.println("šŸ“© Message preview:"); + System.out.println("From: " + senderName); + if (!context.isEmpty()) System.out.println(context); + System.out.println("Time: " + time); + System.out.println("Content: \"" + content + "\""); System.out.println("šŸ” Looking for chat with displayId='" + receiverId + "' type='" + receiverType + "'"); @@ -694,6 +753,7 @@ public class ActionHandler { openChat(chat); } + default -> System.out.println("No interaction available for type: " + type); } } @@ -1182,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(); @@ -1228,8 +1289,11 @@ public class ActionHandler { return true; } + case "7" ->{ + viewMessagesInChat(chat); + } - case "7" -> { + case "8" -> { return false; } default -> System.out.println("Invalid choice."); @@ -1295,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"); @@ -1349,6 +1414,10 @@ public class ActionHandler { toggleArchive(chat.getId() , "group"); } + case "13" ->{ + viewMessagesInChat(chat); + } + case "0" -> { return false; } @@ -1416,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(); @@ -1498,7 +1568,9 @@ public class ActionHandler { case "12"->{ toggleArchive(chat.getId() , "channel"); } - + case "13" ->{ + viewMessagesInChat(chat); + } case "0" -> { return false; @@ -2959,7 +3031,7 @@ public class ActionHandler { } case "2" -> { addContact(chat.getId()); - refreshChatList(); + refreshContactList(); } default -> System.out.println("Back..."); } @@ -3213,9 +3285,396 @@ public class ActionHandler { } } + private void refreshContactList() { + JSONObject req = new JSONObject(); + req.put("action", "get_contact_list"); + req.put("user_id", Session.currentUser.getString("user_id")); + out.println(req.toString()); + + try { + JSONObject response = TelegramClient.responseQueue.take(); + + if (response != null && response.getString("status").equals("success")) { + if (response.has("data") && !response.isNull("data")) { + JSONObject data = response.getJSONObject("data"); + + if (!data.has("contact_list") || data.isNull("contact_list")) { + System.out.println("āŒ contact_list not found in response data."); + return; + } + + JSONArray contactListJson = data.getJSONArray("contact_list"); + List contactList = new ArrayList<>(); + + for (Object obj : contactListJson) { + JSONObject c = (JSONObject) obj; + + ContactEntry entry = new ContactEntry( + UUID.fromString(c.getString("contact_id")), + c.getString("user_id"), + c.getString("profile_name"), + c.optString("image_url", ""), + c.optBoolean("is_blocked", false) + ); + + contactList.add(entry); + } + + Session.contactEntries = contactList; + System.out.println("āœ… Contact list updated. Total: " + contactList.size()); + + } else { + System.out.println("āš ļø Response has no data object."); + } + } else { + if (response.has("message") && !response.isNull("message")) { + System.out.println("āŒ Failed to refresh contact list: " + response.getString("message")); + } else { + System.out.println("āŒ Failed to refresh contact list."); + } + } + } catch (Exception e) { + System.err.println("āŒ Error during refreshContactList: " + e.getMessage()); + e.printStackTrace(); + } + } + + 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 editedAt = msg.optString("edited_at", ""); + String label = isEdited ? "šŸ–Šļø (edited at: " + editedAt + ")" : ""; + + // āœ… Forwarded info + String forwardLabel = ""; + if (msg.optBoolean("is_forwarded", false)) { + String forwardFrom = msg.optString("forwarded_from_name", "Unknown"); + forwardLabel = "šŸ” Forwarded from " + forwardFrom; + } + + //for reply + String replyLabel = ""; + if (msg.has("reply_to_id")) { + String repliedSender = msg.optString("reply_to_sender", "Unknown"); + String repliedContent = msg.optString("reply_to_content", "..."); + replyLabel = "ā†Ŗļø Reply to " + repliedSender + ": \"" + repliedContent + "\""; + } + + JSONArray reactions = msg.optJSONArray("reactions"); + if (reactions != null && !reactions.isEmpty()) { + System.out.print(" šŸ’¬ Reactions: "); + for (int j = 0; j < reactions.length(); j++) { + System.out.print(reactions.getString(j) + " "); + } + System.out.println(); + } + System.out.printf("[%d] [%s] %s: \n", i + 1, time, senderName); + if (!forwardLabel.isEmpty()) System.out.println(forwardLabel); + if (!replyLabel.isEmpty()) System.out.println(replyLabel); + System.out.printf("%s %s\n", content, label); + System.out.println("─────────────────────────────────────────────"); + } + System.out.print(""" + šŸ’¬ Options: + [number] - Interact with message + N - Next page (older messages) + 0 - Back to chat menu + S - Send message + āž¤ Choice: """); + + String input = scanner.nextLine().trim(); + + if (input.equals("0")) return; + if (input.equalsIgnoreCase("N")) { + offset += limit; + continue; + } + + if(input.equalsIgnoreCase("S")){ + sendMessage(chat.getId(), chat.getType()); + } + 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")); + boolean isSender = senderId.toString().equals(Session.currentUser.getString("internal_uuid")); + boolean isChannel = chat.getType().equals("channel"); + boolean isOwnerOrAdmin = chat.isOwner() || chat.isAdmin(); + + System.out.println("\nšŸŽÆ Selected message by " + selected.getString("sender_name")); + + // āœ” Show interaction menu based on access + System.out.println("2. Forward"); + + if (isChannel) { + if (isOwnerOrAdmin) { + System.out.println("1. Reply"); + System.out.println("3. React"); + if (isSender) { + System.out.println("4. Edit"); + } + System.out.println("5. Delete"); + } + } else { + System.out.println("1. Reply"); + System.out.println("3. React"); + if (isSender) { + System.out.println("4. Edit"); + } + if (isSender || isOwnerOrAdmin) { + System.out.println("5. Delete"); + } + } + + System.out.println("0. Back to message list"); + System.out.print("āž¤ Select an action: "); + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1" -> { + if (!isChannel || isOwnerOrAdmin) + replyToMessage(messageId); + else + System.out.println("āŒ You are not allowed to reply."); + } + case "2" -> forwardMessage(messageId); + case "3" -> { + if (!isChannel || isOwnerOrAdmin) + reactToMessage(messageId); + else + System.out.println("āŒ You are not allowed to react."); + } + case "4" -> { + if (isSender) + editMessage(messageId); + else + System.out.println("āŒ You can only edit your own messages."); + } + case "5" -> { + if (!isChannel || isOwnerOrAdmin || isSender) + deleteMessage(messageId); + else + System.out.println("āŒ You are not allowed to delete this message."); + } + case "0" -> {} + default -> System.out.println("āŒ Invalid option."); + } + + } catch (NumberFormatException e) { + System.out.println("āŒ Please enter a valid number or command."); + } + } + } + + private void editMessage(UUID messageId) { + System.out.print("šŸ“ Enter new content: "); + String newContent = scanner.nextLine().trim(); + + if (newContent.isEmpty()) { + System.out.println("āŒ Content cannot be empty."); + return; + } + + JSONObject req = new JSONObject(); + req.put("action", "edit_message"); + req.put("message_id", messageId.toString()); + req.put("new_content", newContent); + + JSONObject res = sendWithResponse(req); + if (res == null || !res.getString("status").equals("success")) { + System.out.println("āŒ Failed to edit message."); + return; + } + + System.out.println("āœ… Message updated successfully."); + } + + + private void reactToMessage(UUID messageId) { + System.out.print("😊 Enter your reaction (e.g., ā¤ļø, šŸ‘, šŸ˜‚): "); + String reaction = scanner.nextLine().trim(); + + if (reaction.isEmpty()) { + System.out.println("āš ļø Empty reaction discarded."); + return; + } + + JSONObject req = new JSONObject(); + req.put("action", "react_to_message"); + req.put("message_id", messageId.toString()); + req.put("reaction", reaction); + + JSONObject res = sendWithResponse(req); + if (res != null && res.getString("status").equals("success")) { + System.out.println("āœ… Reaction sent."); + } else { + System.out.println("āŒ Failed to react to message."); + } + } + + + private void forwardMessage(UUID originalMessageId) { + System.out.println("\nšŸ“¤ Select a chat to forward this message to:"); + + List chatList = Session.chatList; + for (int i = 0; i < chatList.size(); i++) { + ChatEntry c = chatList.get(i); + System.out.printf("%d. [%s] %s\n", i + 1, c.getType(), c.getName()); + } + System.out.println("0. Cancel"); + + System.out.print("āž¤ Enter chat number: "); + String input = scanner.nextLine().trim(); + + if (input.equals("0")) return; + + try { + int choice = Integer.parseInt(input); + if (choice < 1 || choice > chatList.size()) { + System.out.println("āŒ Invalid choice."); + return; + } + + ChatEntry target = chatList.get(choice - 1); + + JSONObject req = new JSONObject(); + req.put("action", "forward_message"); + req.put("target_chat_id", target.getId()); + req.put("target_chat_type", target.getType()); + req.put("original_message_id", originalMessageId.toString()); + req.put("forwarded_by", Session.currentUser.getString("internal_uuid")); + + JSONObject res = sendWithResponse(req); + if (res != null && res.getString("status").equals("success")) { + System.out.println("āœ… Message forwarded successfully."); + } else { + System.out.println("āŒ Failed to forward the message."); + } + + } catch (NumberFormatException e) { + System.out.println("āŒ Invalid input."); + } + } + + + private void replyToMessage(UUID messageId) { + System.out.print("šŸ’¬ Enter your reply: "); + String content = scanner.nextLine().trim(); + + if (content.isEmpty()) { + System.out.println("āš ļø Empty reply discarded."); + return; + } + + UUID chatId = UUID.fromString(Session.currentChatId); + String chatType = Session.currentChatType; + + JSONObject req = new JSONObject(); + req.put("action", "send_reply_message"); + req.put("receiver_type", chatType); + req.put("receiver_id", chatId.toString()); + req.put("content", content); + req.put("reply_to_id", messageId.toString()); + + JSONObject res = sendWithResponse(req); + + if (res != null && res.getString("status").equals("success")) { + System.out.println("āœ… Reply sent."); + } else { + System.out.println("āŒ Failed to send reply."); + } + } + + + 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/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 5168ba9..e6882a2 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -74,24 +74,24 @@ public class IncomingMessageListener implements Runnable { private boolean isRealTimeEvent(String action) { return switch (action) { - case "new_message", "message_edited", "message_deleted", + case "new_message", "message_edited", "message_deleted_global", "user_status_changed", "added_to_group", "added_to_channel", "update_group_or_channel", "chat_deleted", "blocked_by_user", "unblocked_by_user", "message_seen", "removed_from_group", "removed_from_channel", - "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> true; + "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted" -> true; default -> false; }; } void handleRealTimeEvent(JSONObject response) throws IOException { String action = response.getString("action"); - JSONObject msg = response.getJSONObject("data"); + JSONObject msg = response.has("data") ? response.getJSONObject("data") : new JSONObject(); switch (action) { case "added_to_group", "added_to_channel", - "removed_from_group", "removed_from_channel", "chat_deleted" -> { + "removed_from_group", "removed_from_channel", "chat_deleted","created_private_chat" -> { System.out.println("šŸ”„ Chat list changed. Updating..."); Session.forceRefreshChatList = true; System.out.println("🧪 Calling requestChatList() after being added"); @@ -135,10 +135,6 @@ public class IncomingMessageListener implements Runnable { } -// - - - default -> displayRealTimeMessage(action, msg); } @@ -298,36 +294,52 @@ public class IncomingMessageListener implements Runnable { private void displayRealTimeMessage(String action, JSONObject msg) { switch (action) { case "new_message" -> { - System.out.println("\nšŸ”” New Message Received:"); - String senderName = msg.optString("sender_name", "Unknown"); - String content = msg.optString("content", "(empty)"); - String sendAt = msg.optString("send_at", "-"); + String senderName = msg.optString("sender_name","Unknown"); + String content = msg.optString("content","(empty)"); + String sendAt = msg.optString("send_at","-"); + String chatId = msg.optString("receiver_id", msg.optString("chat_id","")); + String kind = msg.optString("kind","plain"); + JSONObject meta = msg.optJSONObject("meta"); - String receiverId = msg.optString("receiver_id", ""); - String receiverType = msg.optString("receiver_type", ""); + String prefix = ""; + if ("reply".equals(kind) && meta != null) { + var rt = meta.optJSONObject("reply_to"); + prefix = "[reply → " + (rt!=null?rt.optString("excerpt",""):"") + "] "; + } else if ("forward".equals(kind) && meta != null) { + var ff = meta.optJSONObject("forwarded_from"); + prefix = "[forwarded from " + (ff!=null?ff.optString("sender_name","unknown"):"unknown") + "] "; + } boolean isInCurrentChat = Session.inChatMenu && - Session.currentChatId != null && - Session.currentChatId.equals(receiverId); + Session.currentChatId != null && Session.currentChatId.equals(chatId); if (isInCurrentChat) { - System.out.println(senderName + ": " + content + " (" + sendAt + ")"); + System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")"); } else { - System.out.println("šŸ’¬ Message from " + senderName + " in " + receiverType + " chat: " + content); + System.out.println("šŸ’¬ Message from " + senderName + ": " + prefix + content); Session.forceRefreshChatList = true; } } + case "message_edited" -> { System.out.println("\nāœļø Message Edited:"); System.out.println("ID: " + msg.getString("message_id")); System.out.println("New Content: " + msg.getString("new_content")); System.out.println("Edit Time: " + msg.getString("edited_at")); } - case "message_deleted" -> { + case "message_deleted_global" -> { System.out.println("\nšŸ—‘ļø Message Deleted:"); System.out.println("Message ID: " + msg.getString("message_id")); } + case "message_reacted", "message_unreacted" -> { + String mid = msg.getString("message_id"); + String emoji = msg.getString("emoji"); + JSONObject counts = msg.optJSONObject("counts"); + int n = msg.optInt("count_for_emoji", 0); + System.out.println("\n⭐ Reaction update on " + mid + " : " + emoji + " → " + n); + } + case "user_status_changed" -> { System.out.println("\nšŸ”„ User Status Changed:"); System.out.println("User: " + msg.getString("user_id")); diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index 6e5f658..a13892c 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -27,8 +27,7 @@ public class Session { public static String currentChatId = null; public static ChatEntry currentChatEntry = null; public static List contactEntries = new ArrayList<>(); - - + public static boolean inContactListMenu = false; public static String getUserUUID() { diff --git a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java index edcd83c..9a488c5 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java @@ -1,6 +1,7 @@ package org.to.telegramfinalproject.Database; import org.to.telegramfinalproject.Models.Contact; +import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.User; import java.sql.*; @@ -80,7 +81,6 @@ public class ContactDatabase { } return !currentlyBlocked; } else { - // Ų§ŚÆŲ± رابطه وجود Ł†ŲÆŲ§Ų±Ł‡ŲŒ Ų§ŁˆŁ„ باید کاربر رو به contact ها اضافه Ś©Ł†ŪŒŁ… String insertSql = "INSERT INTO contacts (user_id, contact_id, is_blocked) VALUES (?, ?, ?)"; try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) { insertStmt.setObject(1, userId); @@ -150,8 +150,13 @@ public class ContactDatabase { } } + public boolean eitherBlocks(UUID userA, UUID userB) { + return isBlocked(userA, userB) || isBlocked(userB, userA); + } - public boolean isBlocked(UUID user_id, UUID contact_id) { + + + public static boolean isBlocked(UUID user_id, UUID contact_id) { String sql = "SELECT is_blocked FROM contacts WHERE user_id = ? AND contact_id = ?"; try (Connection connection = getConnection()) { PreparedStatement stmt = connection.prepareStatement(sql); @@ -268,5 +273,39 @@ public class ContactDatabase { } } + public static List getContactEntries(UUID userId) { + List entries = new ArrayList<>(); + + String sql = """ + SELECT c.contact_id, c.is_blocked, u.user_id, u.profile_name, u.image_url + FROM contacts c + JOIN users u ON c.contact_id = u.internal_uuid + WHERE c.user_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ResultSet rs = ps.executeQuery(); + + while (rs.next()) { + UUID contactId = UUID.fromString(rs.getString("contact_id")); + boolean isBlocked = rs.getBoolean("is_blocked"); + String userIdStr = rs.getString("user_id"); + String profileName = rs.getString("profile_name"); + String imageUrl = rs.getString("image_url"); + + ContactEntry entry = new ContactEntry(contactId, userIdStr, profileName, imageUrl, isBlocked); + entries.add(entry); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return entries; + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index fcdaf7c..db997ee 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -6,6 +6,7 @@ import org.to.telegramfinalproject.Models.Message; import java.sql.*; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; @@ -76,7 +77,7 @@ public class MessageDatabase { } public static void logDeletedMessagesFor(UUID chatId, UUID userId) { - List messages = MessageDatabase.privateChatHistory(chatId); + List messages = MessageDatabase.privateChatHistory(chatId, userId); for (Message message : messages) { if (!isMessageDeleted(message.getMessage_id(), userId)) { String sql = """ @@ -110,6 +111,258 @@ 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, + rs.getTimestamp("edited_at") != null + ? rs.getTimestamp("edited_at").toLocalDateTime() + : 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 static boolean updateContentAndMarkEdited(UUID messageId, String newContent) { + String sql = """ + UPDATE messages + SET content = ?, is_edited = TRUE, edited_at = CURRENT_TIMESTAMP + WHERE message_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, newContent); + ps.setObject(2, messageId); + + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean saveReplyMessage(Message message) { + String sql = """ + INSERT INTO messages ( + message_id, sender_id, receiver_type, receiver_id, content, message_type, + send_at, status, reply_to_id, is_edited, is_deleted_globally, + original_message_id, forwarded_by, forwarded_from, edited_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, message.getMessage_id()); + ps.setObject(2, message.getSender_id()); + ps.setString(3, message.getReceiver_type()); + ps.setObject(4, message.getReceiver_id()); + ps.setString(5, message.getContent()); + ps.setString(6, message.getMessage_type()); + ps.setTimestamp(7, Timestamp.valueOf(message.getSend_at())); + ps.setString(8, message.getStatus()); + ps.setObject(9, message.getReply_to_id()); //Important part because of reply message id + ps.setBoolean(10, message.isIs_edited()); + ps.setBoolean(11, message.isIs_deleted_globally()); + ps.setObject(12, message.getOriginal_message_id()); + ps.setObject(13, message.getForwarded_by()); + ps.setObject(14, message.getForwarded_from()); + ps.setTimestamp(15, message.getEdited_at() != null ? Timestamp.valueOf(message.getEdited_at()) : null); + + return ps.executeUpdate() > 0; + + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean saveForwardedMessage(Message message) { + String sql = """ + INSERT INTO messages ( + message_id, sender_id, receiver_type, receiver_id, + content, message_type, send_at, status, + reply_to_id, is_edited, is_deleted_globally, + original_message_id, forwarded_by, forwarded_from + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, message.getMessage_id()); + ps.setObject(2, message.getSender_id()); + ps.setString(3, message.getReceiver_type()); + ps.setObject(4, message.getReceiver_id()); + ps.setString(5, message.getContent()); + ps.setString(6, message.getMessage_type()); + ps.setTimestamp(7, Timestamp.valueOf(message.getSend_at())); + ps.setString(8, message.getStatus()); + ps.setObject(9, message.getReply_to_id()); + ps.setBoolean(10, message.isIs_edited()); + ps.setBoolean(11, message.isIs_deleted_globally()); + ps.setObject(12, message.getOriginal_message_id()); //original id + ps.setObject(13, message.getForwarded_by()); //forwarded by + ps.setObject(14, message.getForwarded_from()); //forwarded from + + return ps.executeUpdate() > 0; + + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static String getExcerpt(UUID messageId) { + return getExcerpt(messageId, 80); + } + + public static String getExcerpt(UUID messageId, int maxLen) { + String sql = "SELECT content, message_type, is_deleted_globally FROM messages WHERE message_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, messageId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return "Message isn't available"; + + String content = rs.getString("content"); + String type = rs.getString("message_type"); // e.g. TEXT, IMAGE, VIDEO, AUDIO, FILE, STICKER... + boolean deleted = rs.getBoolean("is_deleted_globally"); + + if (deleted) return "This message was deleted"; + + if (type == null || type.equalsIgnoreCase("TEXT")) { + if (content == null || content.isBlank()) return "(empty)"; + return shorten(content, maxLen); + } + + //for media + switch (type.toUpperCase()) { + case "IMAGE": return "[Photo]"; + case "VIDEO": return "[Video]"; + case "AUDIO": return "[Audio]"; + case "VOICE": return "[Voice]"; + case "FILE": return "[File]"; + case "STICKER":return "[Sticker]"; + default: return "[" + type + "]"; + } + } + } catch (SQLException e) { + e.printStackTrace(); + return "Error"; + } + } + + private static String shorten(String s, int maxLen) { + if (s.length() <= maxLen) return s; + return s.substring(0, Math.max(0, maxLen - 1)).trim() + "…"; + } @@ -128,38 +381,43 @@ public class MessageDatabase { - public static List getUnreadMessages(UUID userId) { List messages = new ArrayList<>(); String sql = """ - SELECT m.* FROM messages m + SELECT m.* + FROM messages m LEFT JOIN message_receipts r ON m.message_id = r.message_id AND r.user_id = ? + LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ? WHERE r.user_id IS NULL - AND ( - (m.receiver_type = 'private' AND m.receiver_id = ?) - OR - (m.receiver_type = 'group' AND EXISTS ( - SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ? - )) - OR - (m.receiver_type = 'channel' AND EXISTS ( - SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ? - )) - ) + AND d.message_id IS NULL + AND m.is_deleted_globally = FALSE + AND ( + (m.receiver_type = 'private' AND m.receiver_id = ?) + OR + (m.receiver_type = 'group' AND EXISTS ( + SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ? + )) + OR + (m.receiver_type = 'channel' AND EXISTS ( + SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ? + )) + ) + ORDER BY m.send_at DESC """; try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setObject(1, userId); - stmt.setObject(2, userId); - stmt.setObject(3, userId); - stmt.setObject(4, userId); + stmt.setObject(1, userId); // for message_receipts + stmt.setObject(2, userId); // for deleted_messages + stmt.setObject(3, userId); // for private messages + stmt.setObject(4, userId); // for group members + stmt.setObject(5, userId); // for channel subscribers ResultSet rs = stmt.executeQuery(); while (rs.next()) { - messages.add(new Message( + Message message = new Message( UUID.fromString(rs.getString("message_id")), rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, rs.getString("receiver_type"), @@ -170,10 +428,13 @@ public class MessageDatabase { 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(message); } } catch (SQLException e) { @@ -183,6 +444,8 @@ public class MessageDatabase { return messages; } + + public static List getAttachments(UUID messageId) { List attachments = new ArrayList<>(); String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?"; @@ -271,6 +534,7 @@ public class MessageDatabase { rs.getString("status"), (UUID) rs.getObject("reply_to_id"), rs.getBoolean("is_edited"), + rs.getBoolean("is_deleted_globally"), (UUID) rs.getObject("original_message_id"), (UUID) rs.getObject("forwarded_by"), (UUID) rs.getObject("forwarded_from") @@ -279,45 +543,129 @@ public class MessageDatabase { + +// public static List searchMessagesForUser(UUID userId, String keyword) { +// List result = new ArrayList<>(); +// String sql = """ +// SELECT * FROM messages +// WHERE receiver_type = 'private' +// AND (sender_id = ? OR receiver_id = ?) +// AND content ILIKE ? +// ORDER BY send_at DESC +// """; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// stmt.setObject(1, userId); +// stmt.setObject(2, userId); +// stmt.setString(3, "%" + keyword + "%"); +// +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// result.add(extractMessage(rs)); +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// +// return result; +// } + public static List searchMessagesForUser(UUID userId, String keyword) { + String sql = """ + SELECT * FROM messages m + WHERE m.receiver_type = 'private' + AND m.content ILIKE ? + AND m.is_deleted_globally = FALSE + AND (m.sender_id = ? OR m.receiver_id = ?) + 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 + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, "%" + keyword + "%"); + ps.setObject(2, userId); // m.sender_id + ps.setObject(3, userId); // m.receiver_id + ps.setObject(4, userId); // deleted_messages + + ResultSet rs = ps.executeQuery(); + List messages = new ArrayList<>(); + while (rs.next()) { + messages.add(mapResultSetToMessage(rs)); + } + return messages; + + } catch (SQLException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + } + + private static Message mapResultSetToMessage(ResultSet rs) throws SQLException { + return new Message( + UUID.fromString(rs.getString("message_id")), + rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, + 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 + ); + } + + + +// public static List privateChatHistory(UUID chatId) { +// List result = new ArrayList<>(); +// String sql = """ +// SELECT * FROM messages +// WHERE receiver_type = 'private' +// AND receiver_id = ? +// ORDER BY send_at +// """; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// stmt.setObject(1, chatId); +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// result.add(extractMessage(rs)); +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// return result; +// } + + public static List privateChatHistory(UUID chatId, UUID userId) { List result = new ArrayList<>(); String sql = """ SELECT * FROM messages WHERE receiver_type = 'private' - AND (sender_id = ? OR receiver_id = ?) - AND content ILIKE ? - ORDER BY send_at DESC - """; - - try (Connection conn = ConnectionDb.connect(); - PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setObject(1, userId); - stmt.setObject(2, userId); - stmt.setString(3, "%" + keyword + "%"); - - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - result.add(extractMessage(rs)); - } - } catch (SQLException e) { - e.printStackTrace(); - } - - return result; - } - - public static List privateChatHistory(UUID chatId) { - List result = new ArrayList<>(); - String sql = """ - SELECT * FROM messages - WHERE receiver_type = 'private' - AND receiver_id = ? + AND receiver_id = ? + AND is_deleted_globally = FALSE + AND message_id NOT IN ( + SELECT message_id FROM deleted_messages WHERE user_id = ? + ) ORDER BY send_at """; try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setObject(1, chatId); + stmt.setObject(2, userId); ResultSet rs = stmt.executeQuery(); while (rs.next()) { result.add(extractMessage(rs)); @@ -331,17 +679,47 @@ public class MessageDatabase { - public static List groupChatHistory(UUID groupId) { + +// public static List groupChatHistory(UUID groupId) { +// List result = new ArrayList<>(); +// String sql = """ +// SELECT * FROM messages +// WHERE receiver_type = 'group' AND receiver_id = ? +// ORDER BY send_at +// """; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// stmt.setObject(1, groupId); +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// result.add(extractMessage(rs)); +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// return result; +// } + + + public static List groupChatHistory(UUID groupId,UUID userId) { List result = new ArrayList<>(); String sql = """ - SELECT * FROM messages - WHERE receiver_type = 'group' AND receiver_id = ? + SELECT * FROM messages + WHERE receiver_type = 'group' + AND receiver_id = ? + AND is_deleted_globally = FALSE + AND message_id NOT IN ( + SELECT message_id FROM deleted_messages WHERE user_id = ? + ) ORDER BY send_at """; try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setObject(1, groupId); + stmt.setObject(2, userId); + ResultSet rs = stmt.executeQuery(); while (rs.next()) { result.add(extractMessage(rs)); @@ -353,17 +731,48 @@ public class MessageDatabase { } - public static List channelChatHistory(UUID channelId) { + +// public static List channelChatHistory(UUID channelId) { +// List result = new ArrayList<>(); +// String sql = """ +// SELECT * FROM messages +// WHERE receiver_type = 'channel' AND receiver_id = ? +// ORDER BY send_at +// """; +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// stmt.setObject(1, channelId); +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// result.add(extractMessage(rs)); +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// return result; +// } + + + public static List channelChatHistory(UUID channelId, UUID userId) { List result = new ArrayList<>(); String sql = """ - SELECT * FROM messages - WHERE receiver_type = 'channel' AND receiver_id = ? + SELECT * FROM messages + WHERE receiver_type = 'channel' + AND receiver_id = ? + AND is_deleted_globally = FALSE + AND message_id NOT IN ( + SELECT message_id FROM deleted_messages WHERE user_id = ? + ) ORDER BY send_at """; try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, channelId); + stmt.setObject(2, userId); + ResultSet rs = stmt.executeQuery(); while (rs.next()) { result.add(extractMessage(rs)); @@ -375,32 +784,75 @@ public class MessageDatabase { } - public static List searchMessagesInGroups(List groupIds, String keyword) { + +// public static List searchMessagesInGroups(List groupIds, String keyword) { +// List result = new ArrayList<>(); +// if (groupIds.isEmpty()) return result; +// +// String placeholders = groupIds.stream().map(id -> "?").collect(Collectors.joining(", ")); +// String sql = """ +// SELECT * FROM messages +// WHERE receiver_type = 'group' +// AND receiver_id IN (%s) +// AND content ILIKE ? +// ORDER BY send_at DESC +// """.formatted(placeholders); +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// +// int i = 1; +// for (UUID id : groupIds) { +// stmt.setObject(i++, id); +// } +// stmt.setString(i, "%" + keyword + "%"); +// +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// result.add(extractMessage(rs)); +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// +// return result; +// } + + + + public static List searchMessagesInGroups(List groupIds, String keyword, UUID userId) { List result = new ArrayList<>(); if (groupIds.isEmpty()) return result; String placeholders = groupIds.stream().map(id -> "?").collect(Collectors.joining(", ")); String sql = """ - SELECT * FROM messages - WHERE receiver_type = 'group' - AND receiver_id IN (%s) - AND content ILIKE ? - ORDER BY send_at DESC + SELECT m.* + FROM messages m + LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ? + WHERE m.receiver_type = 'group' + AND m.receiver_id IN (%s) + AND d.message_id IS NULL + AND m.is_deleted_globally = FALSE + AND m.content ILIKE ? + ORDER BY m.send_at DESC """.formatted(placeholders); try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { int i = 1; - for (UUID id : groupIds) { - stmt.setObject(i++, id); + stmt.setObject(i++, userId); + for (UUID groupId : groupIds) { + stmt.setObject(i++, groupId); } + stmt.setString(i, "%" + keyword + "%"); ResultSet rs = stmt.executeQuery(); while (rs.next()) { - result.add(extractMessage(rs)); + result.add(mapResultSetToMessage(rs)); } + } catch (SQLException e) { e.printStackTrace(); } @@ -409,32 +861,71 @@ public class MessageDatabase { } +// public static List searchMessagesInChannels(List channelIds, String keyword) { +// List result = new ArrayList<>(); +// if (channelIds.isEmpty()) return result; +// +// String placeholders = channelIds.stream().map(id -> "?").collect(Collectors.joining(", ")); +// String sql = """ +// SELECT * FROM messages +// WHERE receiver_type = 'channel' +// AND receiver_id IN (%s) +// AND content ILIKE ? +// ORDER BY send_at DESC +// """.formatted(placeholders); +// +// try (Connection conn = ConnectionDb.connect(); +// PreparedStatement stmt = conn.prepareStatement(sql)) { +// +// int i = 1; +// for (UUID id : channelIds) { +// stmt.setObject(i++, id); +// } +// stmt.setString(i, "%" + keyword + "%"); +// +// ResultSet rs = stmt.executeQuery(); +// while (rs.next()) { +// result.add(extractMessage(rs)); +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// +// return result; +// } + + + public static List searchMessagesInChannels(List channelIds, String keyword) { List result = new ArrayList<>(); if (channelIds.isEmpty()) return result; String placeholders = channelIds.stream().map(id -> "?").collect(Collectors.joining(", ")); String sql = """ - SELECT * FROM messages - WHERE receiver_type = 'channel' - AND receiver_id IN (%s) - AND content ILIKE ? - ORDER BY send_at DESC + SELECT m.* + FROM messages m + WHERE m.receiver_type = 'channel' + AND m.receiver_id IN (%s) + AND m.is_deleted_globally = FALSE + AND m.content ILIKE ? + ORDER BY m.send_at DESC """.formatted(placeholders); try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { int i = 1; - for (UUID id : channelIds) { - stmt.setObject(i++, id); + for (UUID channelId : channelIds) { + stmt.setObject(i++, channelId); } + stmt.setString(i, "%" + keyword + "%"); ResultSet rs = stmt.executeQuery(); while (rs.next()) { - result.add(extractMessage(rs)); + result.add(mapResultSetToMessage(rs)); } + } catch (SQLException e) { e.printStackTrace(); } @@ -480,5 +971,39 @@ public class MessageDatabase { return messages; } + public static List getMessages(UUID chatId, UUID userId) { + String sql = """ + SELECT * FROM messages + WHERE receiver_id = ? + AND is_deleted_globally = false + AND message_id NOT IN ( + SELECT message_id FROM deleted_messages + WHERE user_id = ? + ) + ORDER BY send_at + """; + + List messages = new ArrayList<>(); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, chatId); + ps.setObject(2, userId); + + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Message m = extractMessage(rs); + messages.add(m); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return messages; + } + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java new file mode 100644 index 0000000..d027a5a --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java @@ -0,0 +1,76 @@ +package org.to.telegramfinalproject.Database; + +import org.json.JSONObject; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class MessageReactionDatabase { + + public static boolean saveOrUpdateReaction(UUID messageId, UUID userId, String reaction) { + String sql = """ + INSERT INTO message_reactions (message_id, user_id, emoji) + VALUES (?, ?, ?) + ON CONFLICT (message_id, user_id) + DO UPDATE SET emoji = EXCLUDED.emoji, reacted_at = CURRENT_TIMESTAMP + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, userId); + ps.setString(3, reaction); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static List getReactions(UUID messageId) { + String sql = "SELECT emoji FROM message_reactions WHERE message_id = ?"; + List reactions = new ArrayList<>(); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + reactions.add(rs.getString("emoji")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return reactions; + } + public static JSONObject getCountsAsJson(UUID messageId) { + String sql = """ + SELECT emoji, COUNT(*) AS c + FROM message_reactions + WHERE message_id = ? + GROUP BY emoji + """; + + JSONObject counts = new JSONObject(); + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, messageId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String emoji = rs.getString("emoji"); + int c = rs.getInt("c"); + counts.put(emoji, c); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + return counts; // Ł…Ų«Ų§Ł„: {"ā¤ļø":2,"šŸ‘":1} + } + +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index 97ddbcf..e82b57c 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -251,7 +251,36 @@ public class PrivateChatDatabase { } + public static UUID findChatBetween(UUID user1, UUID user2) { + String sql = """ + SELECT chat_id FROM private_chat + WHERE (user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?) + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, user1); + ps.setObject(2, user2); + ps.setObject(3, user2); + ps.setObject(4, user1); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return (UUID) rs.getObject("chat_id"); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return null; + } + + public static UUID getOtherParticipant(UUID chatId, UUID me) { + List members = getMembers(chatId); + for (UUID u : members) { + if (!u.equals(me)) return u; + } + return null; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Contact.java b/src/main/java/org/to/telegramfinalproject/Models/Contact.java index 8e78344..d85ae00 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Contact.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Contact.java @@ -40,5 +40,4 @@ public class Contact { - } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java index c0e4341..cd1e7e6 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java @@ -1,5 +1,7 @@ package org.to.telegramfinalproject.Models; +import org.json.JSONObject; + import java.util.UUID; public class ContactEntry { @@ -41,4 +43,16 @@ public class ContactEntry { public String toString() { return profileName + " (" + userId + ")" + (isBlocked ? " [Blocked]" : ""); } + + + public JSONObject toJson() { + JSONObject obj = new JSONObject(); + obj.put("contact_id", contactId.toString()); + obj.put("user_id", userId); + obj.put("profile_name", profileName); + obj.put("image_url", imageUrl); + obj.put("is_blocked", isBlocked); + return obj; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java index 1b97b58..049d582 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java @@ -2,6 +2,7 @@ package org.to.telegramfinalproject.Models; import org.json.JSONArray; import org.json.JSONObject; +import org.to.telegramfinalproject.Database.userDatabase; import java.time.LocalDateTime; import java.util.List; @@ -9,20 +10,43 @@ import java.util.UUID; public class JsonUtil { public static JSONArray contactListToJson(List contacts) { - - JSONArray array = new JSONArray(); + for (Contact contact : contacts) { + UUID contactId = contact.getContact_id(); + User contactUser = userDatabase.findByInternalUUID(contactId); + if (contactUser == null) continue; + JSONObject obj = new JSONObject(); - obj.put("user_id", contact.getUser_id().toString()); - obj.put("contact_id", contact.getContact_id().toString()); + obj.put("contact_id", contactId.toString()); // UUID + obj.put("user_id", contactUser.getUser_id()); // public ID + obj.put("profile_name", contactUser.getProfile_name()); + obj.put("image_url", contactUser.getImage_url()); obj.put("is_blocked", contact.getIs_blocked()); - obj.put("added_at", contact.getAdd_at().toString()); + array.put(obj); } + return array; } + public static JSONArray contactEntryListToJson(List contactEntries) { + JSONArray array = new JSONArray(); + + for (ContactEntry entry : contactEntries) { + JSONObject obj = new JSONObject(); + obj.put("contact_id", entry.getContactId().toString()); + obj.put("user_id", entry.getUserId()); + obj.put("profile_name", entry.getProfileName()); + obj.put("image_url", entry.getImageUrl()); + obj.put("is_blocked", entry.isBlocked()); + array.put(obj); + } + + return array; + } + + diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index a3de400..7f71b28 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -1,29 +1,37 @@ package org.to.telegramfinalproject.Models; import java.time.LocalDateTime; +import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; public class Message { private UUID message_id; private UUID sender_id; - private String receiver_type; // private, group, channel + private String receiver_type; private UUID receiver_id; private String content; - private String message_type; // TEXT, IMAGE, FILE, ... + private String message_type; private LocalDateTime send_at; - private String status; // SENT, DELIVERED, READ + private String status; private UUID reply_to_id; private boolean is_edited; + private boolean is_deleted_globally; private UUID original_message_id; private UUID forwarded_by; private UUID forwarded_from; private List attachments; + private transient String sender_name; + private transient String receiver_name; + private LocalDateTime edited_at; + + + // āœ… Full Constructor public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content, String message_type, LocalDateTime send_at, String status, - UUID reply_to_id, boolean is_edited, UUID original_message_id, - UUID forwarded_by, UUID forwarded_from) { + UUID reply_to_id, boolean is_edited, boolean is_deleted_globally, + UUID original_message_id, UUID forwarded_by, UUID forwarded_from) { this.message_id = message_id; this.sender_id = sender_id; this.receiver_type = receiver_type; @@ -34,127 +42,141 @@ public class Message { this.status = status; this.reply_to_id = reply_to_id; this.is_edited = is_edited; + this.is_deleted_globally = is_deleted_globally; this.original_message_id = original_message_id; this.forwarded_by = forwarded_by; this.forwarded_from = forwarded_from; } - public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType, String content, String messageType, LocalDateTime now) { + + // āœ… Short Constructors + //for normal messages + public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType, + String content, String messageType, LocalDateTime sendAt) { this.message_id = messageId; this.sender_id = senderId; this.receiver_id = receiverId; this.receiver_type = receiverType; this.content = content; this.message_type = messageType; - this.send_at = now; + this.send_at = sendAt; } - - public UUID getMessage_id() { - return message_id; - } - - - public UUID getSender_id() { - return sender_id; - } - - public void setSender_id(UUID sender_id) { - this.sender_id = sender_id; - } - - public String getReceiver_type() { - return receiver_type; - } - - public void setReceiver_type(String receiver_type) { - this.receiver_type = receiver_type; - } - - public UUID getReceiver_id() { - return receiver_id; - } - - public void setReceiver_id(UUID receiver_id) { - this.receiver_id = receiver_id; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { + //for reply messages + public Message(UUID messageId, UUID senderId, String receiverType, UUID receiverId, + String content, String messageType, LocalDateTime sendAt, String status, + UUID replyToId) { + this.message_id = messageId; + this.sender_id = senderId; + this.receiver_type = receiverType; + this.receiver_id = receiverId; this.content = content; - } - - public String getMessage_type() { - return message_type; - } - - public void setMessage_type(String message_type) { - this.message_type = message_type; - } - - public LocalDateTime getSend_at() { - return send_at; - } - - public void setSend_at(LocalDateTime send_at) { - this.send_at = send_at; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { + this.message_type = messageType; + this.send_at = sendAt; this.status = status; + this.reply_to_id = replyToId; + this.is_edited = false; + this.is_deleted_globally = false; } - public UUID getReply_to_id() { - return reply_to_id; + + + + public Message(UUID messageId, UUID senderId, String receiverType, UUID receiverId, + String content, String messageType, LocalDateTime sendAt, String status, + UUID replyToId, boolean isEdited, boolean isDeletedGlobally, + UUID originalMessageId, UUID forwardedBy, UUID forwardedFrom, + LocalDateTime editedAt) { + + this.message_id = messageId; + this.sender_id = senderId; + this.receiver_type = receiverType; + this.receiver_id = receiverId; + this.content = content; + this.message_type = messageType; + this.send_at = sendAt; + this.status = status; + this.reply_to_id = replyToId; + this.is_edited = isEdited; + this.is_deleted_globally = isDeletedGlobally; + this.original_message_id = originalMessageId; + this.forwarded_by = forwardedBy; + this.forwarded_from = forwardedFrom; + this.edited_at = editedAt; } - public void setReply_to_id(UUID reply_to_id) { - this.reply_to_id = reply_to_id; + + + // āœ… Getters & Setters + public UUID getMessage_id() { return message_id; } + public void setMessage_id(UUID messageId) { this.message_id = messageId; } + + public UUID getSender_id() { return sender_id; } + public void setSender_id(UUID sender_id) { this.sender_id = sender_id; } + + public String getReceiver_type() { return receiver_type; } + public void setReceiver_type(String receiver_type) { this.receiver_type = receiver_type; } + + public UUID getReceiver_id() { return receiver_id; } + public void setReceiver_id(UUID receiver_id) { this.receiver_id = receiver_id; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public String getMessage_type() { return message_type; } + public void setMessage_type(String message_type) { this.message_type = message_type; } + + public LocalDateTime getSend_at() { return send_at; } + public void setSend_at(LocalDateTime send_at) { this.send_at = send_at; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public UUID getReply_to_id() { return reply_to_id; } + public void setReply_to_id(UUID reply_to_id) { this.reply_to_id = reply_to_id; } + + public boolean isIs_edited() { return is_edited; } + public void setIs_edited(boolean is_edited) { this.is_edited = is_edited; } + + public boolean isIs_deleted_globally() { return is_deleted_globally; } + public void setIs_deleted_globally(boolean is_deleted_globally) { + this.is_deleted_globally = is_deleted_globally; } - public boolean isIs_edited() { - return is_edited; + public UUID getOriginal_message_id() { return original_message_id; } + public void setOriginal_message_id(UUID original_message_id) { this.original_message_id = original_message_id; } + + public UUID getForwarded_by() { return forwarded_by; } + public void setForwarded_by(UUID forwarded_by) { this.forwarded_by = forwarded_by; } + + public UUID getForwarded_from() { return forwarded_from; } + public void setForwarded_from(UUID forwarded_from) { this.forwarded_from = forwarded_from; } + + public List getAttachments() { return attachments; } + public void setAttachments(List attachments) { this.attachments = attachments; } + public String getSender_name() { + return sender_name; } - public void setIs_edited(boolean is_edited) { - this.is_edited = is_edited; + public void setSender_name(String sender_name) { + this.sender_name = sender_name; } - public UUID getOriginal_message_id() { - return original_message_id; + public String getReceiver_name() { + return receiver_name; } - public void setOriginal_message_id(UUID original_message_id) { - this.original_message_id = original_message_id; + public void setReceiver_name(String receiver_name) { + this.receiver_name = receiver_name; } - public UUID getForwarded_by() { - return forwarded_by; + public LocalDateTime getEdited_at() { + return edited_at; } - public void setForwarded_by(UUID forwarded_by) { - this.forwarded_by = forwarded_by; + public void setEdited_at(LocalDateTime edited_at) { + this.edited_at = edited_at; } - public UUID getForwarded_from() { - return forwarded_from; - } - public void setForwarded_from(UUID forwarded_from) { - this.forwarded_from = forwarded_from; - } - - public void setAttachments(List attachments) { - this.attachments = attachments; - } - public List getAttachments() { - return attachments; - } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index fef8a04..47cce43 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -419,10 +419,14 @@ public class ClientHandler implements Runnable { JSONObject obj = new JSONObject(); obj.put("type", "message"); obj.put("content", m.getContent()); - obj.put("sender", m.getSender_id().toString()); obj.put("time", m.getSend_at().toString()); + obj.put("sender", m.getSender_id() != null ? m.getSender_id().toString() : "N/A"); obj.put("receiver_id", m.getReceiver_id().toString()); obj.put("receiver_type", m.getReceiver_type()); + User senderUser = userDatabase.findByInternalUUID(m.getSender_id()); + String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown"; + obj.put("sender_name", senderName); + results.add(obj); } @@ -439,13 +443,23 @@ public class ClientHandler implements Runnable { } - List groupMessages = MessageDatabase.searchMessagesInGroups(groupIds, keyword); + List groupMessages = MessageDatabase.searchMessagesInGroups(groupIds, keyword, currentUser.getInternal_uuid()); for (Message m : groupMessages) { JSONObject obj = new JSONObject(); obj.put("type", "message"); obj.put("from", "group"); obj.put("content", m.getContent()); obj.put("time", m.getSend_at().toString()); + obj.put("sender", m.getSender_id() != null ? m.getSender_id().toString() : "N/A"); + obj.put("receiver_id", m.getReceiver_id().toString()); + obj.put("receiver_type", "group"); + Group senderGroup = GroupDatabase.findByInternalUUID(m.getReceiver_id()); + String groupName = senderGroup != null ? senderGroup.getGroup_name() : "Unknown"; + obj.put("group_name", groupName); + User senderUser = userDatabase.findByInternalUUID(m.getSender_id()); + String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown"; + obj.put("sender_name", senderName); + results.add(obj); } @@ -456,6 +470,15 @@ public class ClientHandler implements Runnable { obj.put("from", "channel"); obj.put("content", m.getContent()); obj.put("time", m.getSend_at().toString()); + obj.put("sender", m.getSender_id() != null ? m.getSender_id().toString() : "N/A"); + obj.put("receiver_id", m.getReceiver_id().toString()); + obj.put("receiver_type", "channel"); + Channel SenderChannel = ChannelDatabase.findByInternalUUID(m.getReceiver_id()); + String channelName = SenderChannel != null ? SenderChannel.getChannel_name() : "Unknown"; + obj.put("channel_name", channelName); + User senderUser = userDatabase.findByInternalUUID(m.getSender_id()); + String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown"; + obj.put("sender_name", senderName); results.add(obj); } @@ -486,6 +509,7 @@ public class ClientHandler implements Runnable { currentUser.getImage_url(), contactUUID ); + } response = success @@ -1265,7 +1289,7 @@ public class ClientHandler implements Runnable { break; } - messages = MessageDatabase.privateChatHistory(chatId); + messages = MessageDatabase.privateChatHistory(chatId, currentUser.getInternal_uuid()); } @@ -1275,7 +1299,7 @@ public class ClientHandler implements Runnable { response = new ResponseModel("error", "Group not found."); break; } - messages = MessageDatabase.groupChatHistory(group.getInternal_uuid()); + messages = MessageDatabase.groupChatHistory(group.getInternal_uuid(),currentUser.getInternal_uuid()); } @@ -1285,7 +1309,7 @@ public class ClientHandler implements Runnable { response = new ResponseModel("error", "Channel not found."); break; } - messages = MessageDatabase.channelChatHistory(channel.getInternal_uuid()); + messages = MessageDatabase.channelChatHistory(channel.getInternal_uuid(),currentUser.getInternal_uuid()); } default -> { response = new ResponseModel("error", "Invalid receiver type."); @@ -1988,7 +2012,6 @@ public class ClientHandler implements Runnable { } break; - case "get_or_create_private_chat": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); @@ -1999,7 +2022,23 @@ public class ClientHandler implements Runnable { UUID user1 = UUID.fromString(requestJson.getString("user1")); UUID user2 = UUID.fromString(requestJson.getString("user2")); + UUID oldChat = PrivateChatDatabase.findChatBetween(user1, user2); UUID chatId = PrivateChatDatabase.getOrCreateChat(user1, user2); + boolean isNew = oldChat == null; + + if (isNew) { + JSONObject chatPayload = new JSONObject(); + chatPayload.put("action", "created_private_chat"); + + JSONObject chatData = new JSONObject(); + chatData.put("chat_id", chatId.toString()); + chatData.put("chat_type", "private"); + chatData.put("last_message_time", LocalDateTime.now().toString()); + chatPayload.put("data", chatData); + + RealTimeEventDispatcher.sendToUser(user1, chatPayload); + RealTimeEventDispatcher.sendToUser(user2, chatPayload); + } JSONObject data = new JSONObject(); data.put("chat_id", chatId.toString()); @@ -2014,6 +2053,7 @@ public class ClientHandler implements Runnable { + case "get_private_chat_target": { UUID chatId = UUID.fromString(requestJson.getString("chat_id")); userId = currentUser.getInternal_uuid(); @@ -2029,6 +2069,346 @@ public class ClientHandler implements Runnable { break; } + case "get_contact_list": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + List contacts = ContactDatabase.getContacts(currentUser.getInternal_uuid()); + List contactEntries = new ArrayList<>(); + + for (Contact contact : contacts) { + UUID contactId = contact.getContact_id(); + User contactUser = userDatabase.findByInternalUUID(contactId); + if (contactUser == null) continue; + + ContactEntry entry = new ContactEntry( + contactId, + contactUser.getUser_id(), + contactUser.getProfile_name(), + contactUser.getImage_url(), + contact.getIs_blocked() + ); + + contactEntries.add(entry); + } + + // Optional: sort alphabetically + contactEntries.sort(Comparator.comparing(ContactEntry::getProfileName, String.CASE_INSENSITIVE_ORDER)); + + JSONObject data = new JSONObject(); + data.put("contact_list", JsonUtil.contactEntryListToJson(contactEntries)); + + response = new ResponseModel("success", "Contact list refreshed", data); + 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); + int limit = requestJson.optInt("limit", 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 = PrivateChatDatabase.getOtherParticipant( + m.getReceiver_id(), // chat_id + currentUser.getInternal_uuid() // my user uuid + ); + 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()); + obj.put("edited_at", m.getEdited_at() != null ? m.getEdited_at().toString() : JSONObject.NULL); + + //For reply messages + if (m.getReply_to_id() != null) { + Message replied = MessageDatabase.findById(m.getReply_to_id()); + if (replied != null) { + User repliedSender = userDatabase.findByInternalUUID(replied.getSender_id()); + obj.put("reply_to_id", replied.getMessage_id().toString()); + obj.put("reply_to_sender", repliedSender != null ? repliedSender.getProfile_name() : "Unknown"); + obj.put("reply_to_content", replied.getContent()); + } + } + + // For forwarded messages + if (m.getOriginal_message_id() != null && m.getForwarded_from() != null) { + User originalSender = userDatabase.findByInternalUUID(m.getForwarded_from()); + obj.put("is_forwarded", true); + obj.put("forwarded_from_id", m.getForwarded_from().toString()); + obj.put("forwarded_from_name", originalSender != null ? originalSender.getProfile_name() : "Unknown"); + } else { + obj.put("is_forwarded", false); + } + + //For reactions + List reactions = MessageReactionDatabase.getReactions(m.getMessage_id()); + obj.put("reactions", new JSONArray(reactions)); + + + 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."); + Message updated = MessageDatabase.findById(messageId); + List receivers = Receivers.resolveFor(updated.getReceiver_type(), updated.getReceiver_id(), null); + RealTimeEventDispatcher.notifyMessageDeletedGlobal(updated.getReceiver_id(), messageId, receivers); + + } + else{ + response = new ResponseModel("error", "Failed to delete message globally."); + } + break; + } + + response = new ResponseModel("error", "Invalid delete_type."); + break; + } + + case "edit_message" : { + UUID msgId = UUID.fromString(requestJson.getString("message_id")); + String newContent = requestJson.getString("new_content"); + + Message msg = MessageDatabase.findById(msgId); + if (msg == null) { + response = new ResponseModel("error", "Message not found."); + break; + } + + //Only sender can edit + if (!msg.getSender_id().equals(currentUser.getInternal_uuid())) { + response = new ResponseModel("error", "You can only edit your own messages."); + break; + } + + boolean success = MessageDatabase.updateContentAndMarkEdited(msgId, newContent); + if (!success) { + response = new ResponseModel("error", "Failed to update message."); + } else { + response = new ResponseModel("success", "Message edited."); + Message updated = MessageDatabase.findById(msgId); + List receivers = Receivers.resolveFor(updated.getReceiver_type(), updated.getReceiver_id(), null); + RealTimeEventDispatcher.notifyMessageEdited(updated.getReceiver_id(), updated.getMessage_id(), newContent, LocalDateTime.now(), receivers); + + } + break; + } + + case "send_reply_message" : { + UUID senderId = currentUser.getInternal_uuid(); + String content = requestJson.getString("content"); + String receiverType = requestJson.getString("receiver_type"); + UUID receiverId = UUID.fromString(requestJson.getString("receiver_id")); + //For block + if ("private".equals(receiverType) && !canSendToPrivate(receiverId, senderId)) { + response = new ResponseModel("error", "You can't message this user (blocked)."); + break; + } + + UUID replyToId = UUID.fromString(requestJson.getString("reply_to_id")); + + Message message = new Message( + UUID.randomUUID(), + senderId, + receiverType, + receiverId, + content, + "TEXT", + LocalDateTime.now(), + "SEND", + replyToId + ); + + boolean saved = MessageDatabase.saveReplyMessage(message); + + String excerpt = MessageDatabase.getExcerpt(replyToId); + JSONObject meta = new JSONObject().put("reply_to", new JSONObject() + .put("id", replyToId.toString()) + .put("excerpt", excerpt)); + + List receivers = Receivers.resolveFor(receiverType, receiverId, senderId); + //RealTimeEventDispatcher.sendNewMessage(message, receivers, "reply", meta); + RealTimeEventDispatcher.sendNewMessageFiltered(message, receivers, senderId, "reply", meta); + + + response = saved ? + new ResponseModel("success", "Reply sent") : + new ResponseModel("error", "Failed to send reply"); + break; + } + + case "forward_message" : { + UUID senderId = currentUser.getInternal_uuid(); + UUID originalMessageId = UUID.fromString(requestJson.getString("original_message_id")); + UUID targetChatId = UUID.fromString(requestJson.getString("target_chat_id")); + String targetChatType = requestJson.getString("target_chat_type"); + + //For block + if ("private".equals(targetChatType) && !canSendToPrivate(targetChatId, senderId)) { + response = new ResponseModel("error", "You can't message this user (blocked)."); + break; + } + + // original message + Message original = MessageDatabase.findById(originalMessageId); + if (original == null) { + response = new ResponseModel("error", "Original message not found."); + break; + } + + //make forwarded message + Message forwarded = new Message( + UUID.randomUUID(), + currentUser.getInternal_uuid(), + targetChatType, + targetChatId, + original.getContent(), + original.getMessage_type(), + LocalDateTime.now(), + "SEND", + null, // reply_to_id + false, // is_edited + false, // is_deleted_globally + original.getMessage_id(), //original message id + currentUser.getInternal_uuid(), // forwarded_by + original.getSender_id(), // forwarded_from + null // edited_at + ); + + boolean success = MessageDatabase.saveForwardedMessage(forwarded); + + if (success) { + response = new ResponseModel("success", "Message forwarded."); + JSONObject meta = new JSONObject().put("forwarded_from", new JSONObject() + .put("chat_id", original.getReceiver_id().toString()) + .put("message_id", original.getMessage_id().toString()) + .put("sender_id", original.getSender_id().toString()) + .put("sender_name", userDatabase.findByInternalUUID(original.getSender_id()).getProfile_name())); + + List receivers = Receivers.resolveFor(targetChatType, targetChatId, currentUser.getInternal_uuid()); + //RealTimeEventDispatcher.sendNewMessage(forwarded, receivers, "forward", meta); + RealTimeEventDispatcher.sendNewMessageFiltered(forwarded, receivers, senderId, "forward", meta); + + } else { + response = new ResponseModel("error", "Failed to forward message."); + } + break; + } + + case "react_to_message": { + UUID messageId = UUID.fromString(requestJson.getString("message_id")); + String reaction = requestJson.getString("reaction"); + boolean success = MessageReactionDatabase.saveOrUpdateReaction(messageId, currentUser.getInternal_uuid(), reaction); + + + if (success) { + Message msg = MessageDatabase.findById(messageId); + + JSONObject counts = MessageReactionDatabase.getCountsAsJson(messageId); + + List receivers = Receivers.resolveFor(msg.getReceiver_type(), msg.getReceiver_id(), null); + + RealTimeEventDispatcher.notifyReactionAdded( + msg.getReceiver_id(), // chatId + messageId, // messageId + reaction, // emoji + counts.optInt(reaction, 0), + counts, + receivers + ); + + response = new ResponseModel("success", "Reaction saved."); + } else { + response = new ResponseModel("error", "Failed to save reaction."); + } + + break; + } + @@ -2103,6 +2483,13 @@ public class ClientHandler implements Runnable { if(Objects.equals(receiverType, "private")){ PrivateChatDatabase.clearDeletedFlag(senderId, receiverId); + UUID other = PrivateChatDatabase.getOtherParticipant(receiverId, senderId); + if (other == null) { + return new ResponseModel("error", "Invalid private chat."); + } + if (ContactDatabase.isBlocked(senderId, other) || ContactDatabase.isBlocked(other, senderId)) { + return new ResponseModel("error", "You can't message this user (blocked)."); + } } @@ -2210,6 +2597,14 @@ public class ClientHandler implements Runnable { } } + // helper + private boolean canSendToPrivate(UUID chatId, UUID senderId) { + UUID other = PrivateChatDatabase.getOtherParticipant(chatId, senderId); + if (other == null) return false; + return !(ContactDatabase.isBlocked(senderId, other) || ContactDatabase.isBlocked(other, senderId)); + } + + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java index c9de28d..db8c6a7 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -2,6 +2,7 @@ package org.to.telegramfinalproject.Server; import org.json.JSONObject; import org.to.telegramfinalproject.Database.ChannelDatabase; +import org.to.telegramfinalproject.Database.ContactDatabase; import org.to.telegramfinalproject.Database.GroupDatabase; import org.to.telegramfinalproject.Database.userDatabase; import org.to.telegramfinalproject.Models.Message; @@ -81,33 +82,119 @@ public class RealTimeEventDispatcher { - public static void notifyMessageEdited(UUID messageId, String newContent, List receivers) { - - String editTime = LocalDateTime.now().toString(); + public static void notifyMessageEdited(UUID chatId, UUID messageId, String newContent, LocalDateTime editedAt, List receivers) { JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); data.put("message_id", messageId.toString()); data.put("new_content", newContent); - data.put("edited_at", editTime); + data.put("edited_at", editedAt.toString()); JSONObject event = new JSONObject(); - event.put("action", "edit_message"); + event.put("action", "message_edited"); event.put("data", data); broadcastToUsers(receivers, event); } - public static void notifyMessageDeleted(UUID messageId, List receivers) { + + public static void sendNewMessage(Message message, List receivers, String kind, JSONObject meta) { + JSONObject payload = new JSONObject(); + payload.put("action", "new_message"); + JSONObject data = new JSONObject(); + data.put("id", message.getMessage_id().toString()); + data.put("chat_id", message.getReceiver_id().toString()); + data.put("sender_id", message.getSender_id().toString()); + data.put("receiver_id", message.getReceiver_id().toString()); + data.put("receiver_type", message.getReceiver_type()); + data.put("content", message.getContent()); + data.put("message_type", message.getMessage_type()); + data.put("send_at", message.getSend_at().toString()); + + User sender = userDatabase.findByInternalUUID(message.getSender_id()); + if (sender != null) data.put("sender_name", sender.getProfile_name()); + + data.put("kind", kind == null ? "plain" : kind); // plain|reply|forward + if (meta != null) data.put("meta", meta); // reply_to {...} | forwarded_from {...} + + payload.put("data", data); + for (UUID userId : receivers) sendToUser(userId, payload); + } + + public static void sendNewMessageFiltered(Message m, List receivers, UUID senderId, String kind, JSONObject meta) { + JSONObject payload = new JSONObject() + .put("action", "new_message") + .put("data", new JSONObject() + .put("id", m.getMessage_id().toString()) + .put("message_id", m.getMessage_id().toString()) + .put("sender_id", m.getSender_id().toString()) + .put("sender_name", userDatabase.findByInternalUUID(m.getSender_id()).getProfile_name()) + .put("receiver_id", m.getReceiver_id().toString()) + .put("receiver_type", m.getReceiver_type()) + .put("content", m.getContent()) + .put("message_type", m.getMessage_type()) + .put("send_at", m.getSend_at().toString()) + .put("kind", kind) + .put("meta", meta != null ? meta : JSONObject.NULL) + ); + + for (UUID uid : receivers) { + //If receiver blocked + if ("private".equalsIgnoreCase(m.getReceiver_type()) && ContactDatabase.isBlocked(uid, senderId)) continue; + sendToUser(uid, payload); + } + } + + + public static void notifyMessageDeletedGlobal(UUID chatId, UUID messageId, List receivers) { + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); data.put("message_id", messageId.toString()); JSONObject event = new JSONObject(); - event.put("action", "delete_message"); + event.put("action", "message_deleted_global"); event.put("data", data); broadcastToUsers(receivers, event); } + + public static void notifyReactionAdded(UUID chatId, UUID messageId, String emoji, + int totalForEmoji, JSONObject countsAll, List receivers) { + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); + data.put("message_id", messageId.toString()); + data.put("emoji", emoji); + data.put("counts", countsAll); // {"ā¤ļø":3,"šŸ‘":1,...} + data.put("count_for_emoji", totalForEmoji); + + JSONObject event = new JSONObject(); + event.put("action", "message_reacted"); + event.put("data", data); + + broadcastToUsers(receivers, event); + } + + public static void notifyReactionRemoved(UUID chatId, UUID messageId, String emoji, + int totalForEmoji, JSONObject countsAll, List receivers) { + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); + data.put("message_id", messageId.toString()); + data.put("emoji", emoji); + data.put("counts", countsAll); + data.put("count_for_emoji", totalForEmoji); + + JSONObject event = new JSONObject(); + event.put("action", "message_unreacted"); + event.put("data", data); + + broadcastToUsers(receivers, event); + } + + + + public static void notifyUserUpdated(UUID userId, String newProfileName, String newImageUrl, List contactIds) { JSONObject data = new JSONObject(); data.put("user_id", userId.toString()); @@ -328,6 +415,13 @@ public class RealTimeEventDispatcher { for (UUID userId : receivers) { sendToUser(userId, payload); } + + + for (UUID uid : receivers) { + //If receiver blocked + if ("private".equalsIgnoreCase(message.getReceiver_type()) && ContactDatabase.isBlocked(uid, message.getSender_id())) continue; + sendToUser(uid, payload); + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/Receivers.java b/src/main/java/org/to/telegramfinalproject/Server/Receivers.java new file mode 100644 index 0000000..578cde0 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/Receivers.java @@ -0,0 +1,21 @@ +package org.to.telegramfinalproject.Server; + +import org.to.telegramfinalproject.Database.ChannelDatabase; +import org.to.telegramfinalproject.Database.GroupDatabase; +import org.to.telegramfinalproject.Database.PrivateChatDatabase; + +import java.util.List; +import java.util.UUID; + +public class Receivers { + public static List resolveFor(String type, UUID chatId, UUID exclude) { + List ids = switch (type) { + case "private" -> PrivateChatDatabase.getMembers(chatId); + case "group" -> GroupDatabase.getMemberUUIDs(chatId); + case "channel" -> ChannelDatabase.getSubscriberUUIDs(chatId); + default -> List.of(); + }; + if (exclude != null) ids.remove(exclude); + return ids; + } +} 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; + } }