From 5b9e02bbdf3df621f95225fedfe93d7a14f7e0b0 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 24 Jul 2025 21:48:49 +0330 Subject: [PATCH 01/11] work on archived chats --- .../Client/ActionHandler.java | 141 +++++++++++++++++- .../telegramfinalproject/Client/Session.java | 4 + .../Database/ArchivedChatDatabase.java | 72 +++++++++ .../Database/ConnectionDb.java | 2 +- .../Database/GroupDatabase.java | 6 +- .../Database/userDatabase.java | 2 + .../Models/ChatEntry.java | 11 ++ .../Server/ClientHandler.java | 44 ++++++ 8 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Database/ArchivedChatDatabase.java diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 644a1c2..0351550 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -582,6 +582,40 @@ public class ActionHandler { } } } +// +// public void showChatListAndSelect() { +// if (Session.chatList == null || Session.chatList.isEmpty()) { +// System.out.println("No chats available."); +// return; +// } +// +// System.out.println("\nYour Chats:"); +// for (int i = 0; i < Session.chatList.size(); i++) { +// ChatEntry entry = Session.chatList.get(i); +// String time = (entry.getLastMessageTime() == null) +// ? "No messages yet" +// : entry.getLastMessageTime().toString(); +// System.out.println((i + 1) + ". [" + entry.getType() + "] " + +// entry.getName() + " - Last: " + time); +// } +// +// System.out.print("Select a chat by number: "); +// int choice = Integer.parseInt(scanner.nextLine()) - 1; +// +// if(choice == -1){ +// System.out.println("Exit..."); +// return; +// } +// if (choice < -1 || choice >= Session.chatList.size()) { +// System.out.println("Invalid selection."); +// return; +// } +// +// ChatEntry selected = Session.chatList.get(choice); +// openChat(selected); +// } + + public void showChatListAndSelect() { if (Session.chatList == null || Session.chatList.isEmpty()) { @@ -589,9 +623,15 @@ public class ActionHandler { return; } + List activeChats = Session.chatList.stream() + .filter(c -> !c.isArchived()) + .toList(); + System.out.println("\nYour Chats:"); - for (int i = 0; i < Session.chatList.size(); i++) { - ChatEntry entry = Session.chatList.get(i); + System.out.println("0. 📦 Archived Chats"); + + for (int i = 0; i < activeChats.size(); i++) { + ChatEntry entry = activeChats.get(i); String time = (entry.getLastMessageTime() == null) ? "No messages yet" : entry.getLastMessageTime().toString(); @@ -600,23 +640,28 @@ public class ActionHandler { } System.out.print("Select a chat by number: "); - int choice = Integer.parseInt(scanner.nextLine()) - 1; + int choice = Integer.parseInt(scanner.nextLine()); - if(choice == -1){ - System.out.println("Exit..."); + if (choice == 0) { + showArchivedChats(); return; } - if (choice < -1 || choice >= Session.chatList.size()) { + + int index = choice - 1; + + if (index < 0 || index >= activeChats.size()) { System.out.println("Invalid selection."); return; } - ChatEntry selected = Session.chatList.get(choice); + ChatEntry selected = activeChats.get(index); openChat(selected); } + + private void openChat(ChatEntry chat) { JSONObject req = new JSONObject(); req.put("action", "get_messages"); @@ -640,6 +685,42 @@ public class ActionHandler { } + private void showArchivedChats() { + List archived = Session.chatList.stream() + .filter(ChatEntry::isArchived) + .toList(); + + if (archived.isEmpty()) { + System.out.println("📭 No archived chats."); + return; + } + + System.out.println("\n📦 Archived Chats:"); + for (int i = 0; i < archived.size(); i++) { + ChatEntry entry = archived.get(i); + String time = (entry.getLastMessageTime() == null) + ? "No messages yet" + : entry.getLastMessageTime().toString(); + System.out.println((i + 1) + ". [" + entry.getType() + "] " + + entry.getName() + " - Last: " + time); + } + + System.out.print("Select a chat by number (or 0 to return): "); + int choice = Integer.parseInt(scanner.nextLine()); + + if (choice == 0) { + System.out.println("🔙 Returning to main chat list..."); + return; + } + + if (choice < 1 || choice > archived.size()) { + System.out.println("❌ Invalid selection."); + return; + } + + ChatEntry selected = archived.get(choice - 1); + openChat(selected); + } @@ -1920,4 +2001,50 @@ public class ActionHandler { return null; } } + + private void archiveChat(UUID chatId, String chatType) { + JSONObject req = new JSONObject(); + req.put("action", "archive_chat"); + req.put("chat_id", chatId.toString()); + req.put("chat_type", chatType); + JSONObject res = sendWithResponse(req); + if (res != null) System.out.println(res.getString("message")); + } + + private void unarchiveChat(UUID chatId, String chatType) { + JSONObject req = new JSONObject(); + req.put("action", "unarchive_chat"); + req.put("chat_id", chatId.toString()); + req.put("chat_type", chatType); + JSONObject res = sendWithResponse(req); + if (res != null) System.out.println(res.getString("message")); + } + + + + +// private void toggleArchiveChat(ChatEntry chat) { +// JSONObject req = new JSONObject(); +// if (chat.isArchived()) { +// req.put("action", "unarchive_chat"); +// } else { +// req.put("action", "archive_chat"); +// } +// req.put("chat_id", chat.getId().toString()); +// req.put("chat_type", chat.getType()); +// +// JSONObject res = sendWithResponse(req); +// if (res != null) { +// System.out.println(res.getString("message")); +// +// // تغییر وضعیت آرشیو به‌صورت لوکالی +// chat.setArchived(!chat.isArchived()); +// +// // اگر در لیست اصلی یا آرشیو بود، بعد از تغییر منو رو ببند +// System.out.println("🔄 Returning to chat list..."); +// forceExitChat = true; // از منو خارج بشیم +// } + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index 64270a1..b057624 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -3,13 +3,17 @@ package org.to.telegramfinalproject.Client; import org.json.JSONObject; import org.to.telegramfinalproject.Models.ChatEntry; +import java.util.ArrayList; import java.util.List; +import java.util.UUID; // method for save data from server response public class Session { public static JSONObject currentUser; public static List chatList; + public static List archivedChatIds = new ArrayList<>(); + public static String getUserUUID() { if (currentUser.has("uuid")) return currentUser.getString("uuid"); diff --git a/src/main/java/org/to/telegramfinalproject/Database/ArchivedChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ArchivedChatDatabase.java new file mode 100644 index 0000000..7ad71ba --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/ArchivedChatDatabase.java @@ -0,0 +1,72 @@ +package org.to.telegramfinalproject.Database; + +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 ArchivedChatDatabase { + public static boolean archiveChat(UUID userId, UUID chatId, String chatType) { + String sql = "INSERT INTO archived_chats (user_id, chat_id, chat_type) VALUES (?, ?, ?) ON CONFLICT DO NOTHING"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ps.setObject(2, chatId); + ps.setString(3, chatType); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + + public static boolean unarchiveChat(UUID userId, UUID chatId) { + String sql = "DELETE FROM archived_chats WHERE user_id = ? AND chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ps.setObject(2, chatId); + return ps.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean isChatArchived(UUID userId, UUID chatId) { + String sql = "SELECT 1 FROM archived_chats WHERE user_id = ? AND chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ps.setObject(2, chatId); + ResultSet rs = ps.executeQuery(); + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static List getArchivedChats(UUID userId) { + List list = new ArrayList<>(); + String sql = "SELECT chat_id FROM archived_chats WHERE user_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, userId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + list.add(UUID.fromString(rs.getString("chat_id"))); + } + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + + +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/ConnectionDb.java b/src/main/java/org/to/telegramfinalproject/Database/ConnectionDb.java index 8d72664..b58f4c0 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ConnectionDb.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ConnectionDb.java @@ -7,7 +7,7 @@ import java.sql.SQLException; public class ConnectionDb { private static final String JDBC_URL = "jdbc:postgresql://localhost:5432/Telegram"; private static final String USERNAME = "postgres"; - private static final String PASSWORD = "124postpass"; + private static final String PASSWORD = "Partow@1384"; public ConnectionDb() { } diff --git a/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java index 008a147..46e1520 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java @@ -444,7 +444,7 @@ public class GroupDatabase { stmt.setObject(2, userId); ResultSet rs = stmt.executeQuery(); - return rs.next(); // اگر رکوردی پیدا شد یعنی owner است + return rs.next(); } catch (SQLException e) { e.printStackTrace(); } @@ -460,7 +460,7 @@ public class GroupDatabase { ResultSet rs = stmt.executeQuery(); if (rs.next()) { String role = rs.getString("role"); - return "admin".equals(role) || "owner".equals(role); // owner هم admin هست + return "admin".equals(role) || "owner".equals(role); } } catch (SQLException e) { e.printStackTrace(); @@ -508,7 +508,7 @@ public class GroupDatabase { while (rs.next()) { JSONObject member = new JSONObject(); member.put("profile_name", rs.getString("profile_name")); - member.put("user_id", rs.getString("user_id")); // آیدی قابل نمایش + member.put("user_id", rs.getString("user_id")); member.put("internal_uuid", rs.getObject("internal_uuid").toString()); member.put("role", rs.getString("role")); diff --git a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java index a330da4..4d9d998 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java @@ -264,6 +264,8 @@ public class userDatabase { return null; } + + public List searchUsers(String keyword, UUID currentUserId) { String query = """ SELECT * FROM users diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index cbd0940..731999c 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -87,4 +87,15 @@ public class ChatEntry { public void setPermissions(JSONObject permissions) { this.permissions = permissions; } + + private boolean archived = false; + + public boolean isArchived() { + return archived; + } + + public void setArchived(boolean archived) { + this.archived = archived; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 76022b6..d6541de 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1189,6 +1189,50 @@ public class ClientHandler implements Runnable { break; } + case "archive_chat": { + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + String chatType = requestJson.getString("chat_type"); + + boolean success = ArchivedChatDatabase.archiveChat(currentUser.getInternal_uuid(), chatId, chatType); + response = success + ? new ResponseModel("success", "Chat archived successfully.") + : new ResponseModel("error", "Failed to archive chat."); + break; + } + + case "unarchive_chat": { + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + + boolean success = ArchivedChatDatabase.unarchiveChat(currentUser.getInternal_uuid(), chatId); + response = success + ? new ResponseModel("success", "Chat unarchived successfully.") + : new ResponseModel("error", "Failed to unarchive chat."); + break; + } + + case "get_archived_chats": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + List archived = ArchivedChatDatabase.getArchivedChats(currentUser.getInternal_uuid()); + + boolean success = archived != null; + + response = success + ? new ResponseModel( + "success", + "Archived chat list fetched successfully.", + new JSONObject().put("archived", new JSONArray(archived.stream().map(UUID::toString).toList())) + ) + : new ResponseModel("error", "Failed to fetch archived chats."); + + break; + } + + + default: From 775350f8ce1bd74698a6394e81e8ceb0729d82dc Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Sun, 27 Jul 2025 16:15:25 +0330 Subject: [PATCH 02/11] work on archived chats --- .../Client/ActionHandler.java | 216 ++++++++++++++++-- .../telegramfinalproject/Client/Session.java | 13 ++ .../Models/ChatEntry.java | 13 +- .../Server/ClientHandler.java | 139 ++++++++++- 4 files changed, 356 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 5d51ac6..6173d93 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -440,8 +440,11 @@ public class ActionHandler { Session.currentUser = response.getJSONObject("data"); JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); + JSONArray Archived = Session.currentUser.getJSONArray("archived_chat_list"); + JSONArray Active = Session.currentUser.getJSONArray("active_chat_lis"); List chatList = new ArrayList<>(); + for (Object obj : chatListJson) { JSONObject chat = (JSONObject) obj; @@ -462,7 +465,48 @@ public class ActionHandler { } + List archivedChats = new ArrayList<>(); + for (Object obj : Archived){ + 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) + ); + archivedChats.add(entry); + + } + + List activeChats = new ArrayList<>(); + for (Object obj : Active){ + 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) + ); + + activeChats.add(entry); + + } + + + + + Session.activeChats = activeChats; + Session.archivedChats = archivedChats; Session.chatList = chatList; break; @@ -725,34 +769,110 @@ public class ActionHandler { } } +// public void showChatListAndSelect() { +// +// +// List chatList = Session.getChatList(); +// if (chatList.isEmpty()) { +// System.out.println("📭 You have no chats."); +// return; +// } +// +// System.out.println("\nYour Chats:"); +// for (int i = 0; i < chatList.size(); i++) { +// ChatEntry entry = chatList.get(i); +// String last = entry.getLastMessageTime() == null ? "No messages yet" : entry.getLastMessageTime().toString(); +// System.out.printf("%d. [%s] %s - Last: %s\n", i + 1, entry.getType(), entry.getName(), last); +// } +// +// System.out.print("Select a chat by number: "); +// int choice = Integer.parseInt(scanner.nextLine()); +// if (choice < 1 || choice > chatList.size()) { +// System.out.println("❌ Invalid choice."); +// return; +// } +// +// openChat(chatList.get(choice - 1)); +// } + + public void showChatListAndSelect() { - - - List chatList = Session.getChatList(); - if (chatList.isEmpty()) { - System.out.println("📭 You have no chats."); + if (Session.activeChats == null || Session.activeChats.isEmpty()) { + System.out.println("No active chats."); return; } System.out.println("\nYour Chats:"); - for (int i = 0; i < chatList.size(); i++) { - ChatEntry entry = chatList.get(i); - String last = entry.getLastMessageTime() == null ? "No messages yet" : entry.getLastMessageTime().toString(); - System.out.printf("%d. [%s] %s - Last: %s\n", i + 1, entry.getType(), entry.getName(), last); + System.out.println("0. 📦 Archived Chats"); + + for (int i = 0; i < Session.activeChats.size(); i++) { + ChatEntry entry = Session.activeChats.get(i); + String time = (entry.getLastMessageTime() == null) + ? "No messages yet" + : entry.getLastMessageTime().toString(); + System.out.println((i + 1) + ". [" + entry.getType() + "] " + + entry.getName() + " - Last: " + time); } System.out.print("Select a chat by number: "); int choice = Integer.parseInt(scanner.nextLine()); - if (choice < 1 || choice > chatList.size()) { - System.out.println("❌ Invalid choice."); + + if (choice == 0) { + showArchivedChats(); return; } - openChat(chatList.get(choice - 1)); + int index = choice - 1; + + if (index < 0 || index >= Session.activeChats.size()) { + System.out.println("Invalid selection."); + return; + } + + ChatEntry selected = Session.activeChats.get(index); + openChat(selected); } + private void showArchivedChats() { + if (Session.archivedChats == null || Session.archivedChats.isEmpty()) { + System.out.println("📭 No archived chats."); + return; + } + + System.out.println("\n📦 Archived Chats:"); + for (int i = 0; i < Session.archivedChats.size(); i++) { + ChatEntry entry = Session.archivedChats.get(i); + String time = (entry.getLastMessageTime() == null) + ? "No messages yet" + : entry.getLastMessageTime().toString(); + System.out.println((i + 1) + ". [" + entry.getType() + "] " + + entry.getName() + " - Last: " + time); + } + + System.out.print("Select a chat by number (or 0 to return): "); + int choice = Integer.parseInt(scanner.nextLine()); + + if (choice == 0) { + System.out.println("🔙 Returning to main chat list..."); + return; + } + + int index = choice - 1; + + if (index < 0 || index >= Session.archivedChats.size()) { + System.out.println("❌ Invalid selection."); + return; + } + + ChatEntry selected = Session.archivedChats.get(index); + openChat(selected); + } + + + + private void openChat(ChatEntry chat) { JSONObject req = new JSONObject(); req.put("action", "get_messages"); @@ -856,7 +976,8 @@ public class ActionHandler { System.out.println("3. Delete chat (one-sided)"); System.out.println("4. Delete chat (both sides)"); System.out.println("5. View profile"); - System.out.println("6. Back"); + System.out.println("6. Archive/Unarchived"); + System.out.println("7. Back"); String input = scanner.nextLine(); @@ -897,7 +1018,13 @@ public class ActionHandler { return true; } - case "6" -> { + case "6" ->{ + toggleArchive(chat.getId() , "private"); + + return true; + } + + case "7" -> { return false; } default -> System.out.println("Invalid choice."); @@ -961,6 +1088,8 @@ public class ActionHandler { System.out.println("11. Edit Admin Permissions"); } + System.out.println("12. Archive/Unarchived"); + System.out.println("0. Back to Chat List"); String input = scanner.nextLine(); @@ -1010,6 +1139,10 @@ public class ActionHandler { } } + case "12"->{ + toggleArchive(chat.getId() , "group"); + } + case "0" -> { return false; } @@ -1076,6 +1209,7 @@ public class ActionHandler { System.out.println("11. Edit Admin Permissions"); } + System.out.println("12. Archive/Unarchived"); System.out.println("0. Back to Chat List"); String input = scanner.nextLine(); @@ -1155,6 +1289,10 @@ public class ActionHandler { } } + case "12"->{ + toggleArchive(chat.getId() , "channel"); + } + case "0" -> { return false; @@ -2714,6 +2852,56 @@ public class ActionHandler { + public void toggleArchive(UUID chatId, String chatType) { + // پیدا کردن چت از Session.chatList + Optional optional = Session.chatList.stream() + .filter(c -> c.getId().equals(chatId)) + .findFirst(); + + if (optional.isEmpty()) { + System.out.println("❌ Chat not found."); + return; + } + + ChatEntry chat = optional.get(); + + if (chat.isArchived()) { + unarchiveChat(chatId, chatType); + chat.setArchived(false); + System.out.println("✅ Chat unarchived."); + } else { + archiveChat(chatId, chatType); + chat.setArchived(true); + System.out.println("✅ Chat archived."); + } + + Session.refreshChatLists(); + } + + + private void archiveChat(UUID chatId, String chatType) { + JSONObject req = new JSONObject(); + req.put("action", "archive_chat"); + req.put("chat_id", chatId.toString()); + req.put("chat_type", chatType); + JSONObject res = sendWithResponse(req); + if (res != null) System.out.println(res.getString("message")); + } + + private void unarchiveChat(UUID chatId, String chatType) { + JSONObject req = new JSONObject(); + req.put("action", "unarchive_chat"); + req.put("chat_id", chatId.toString()); + req.put("chat_type", chatType); + JSONObject res = sendWithResponse(req); + if (res != null) System.out.println(res.getString("message")); + } + + + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index f849e41..c49bb3d 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -13,6 +13,9 @@ import java.util.UUID; public class Session { public static JSONObject currentUser; public static List chatList = new ArrayList<>(); + public static List archivedChats = new ArrayList<>(); + public static List activeChats = new ArrayList<>(); + public static volatile boolean forceRefreshChatList = false; public static volatile boolean backToChatList = false; public static boolean inChatListMenu = false; @@ -55,4 +58,14 @@ public class Session { return chatList; } + + public static void refreshChatLists() { + Session.activeChats = Session.chatList.stream() + .filter(c -> !c.isArchived()) + .toList(); + + Session.archivedChats = Session.chatList.stream() + .filter(ChatEntry::isArchived) + .toList(); + } } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 7491297..b634ce3 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -12,8 +12,9 @@ public class ChatEntry { private String imageUrl; private String type; private LocalDateTime lastMessageTime; + private boolean archived = false; + - // 🔹 نقش‌ها private boolean isOwner = false; private boolean isAdmin = false; private JSONObject permissions; @@ -28,7 +29,6 @@ public class ChatEntry { this.lastMessageTime = lastMessageTime; } - // ✅ کانستراکتور اضافه‌شده برای پشتیبانی از نقش‌ها (اختیاری، برای استفاده‌های جدید) public ChatEntry(UUID internalId, String displayId, String name, String imageUrl, String type, LocalDateTime lastMessageTime, boolean isOwner, boolean isAdmin) { this(internalId, displayId, name, imageUrl, type, lastMessageTime); this.isOwner = isOwner; @@ -41,7 +41,6 @@ public class ChatEntry { } - // 🟩 گتر و ستر جدید public boolean isOwner() { return isOwner; } @@ -108,4 +107,12 @@ public class ChatEntry { + public boolean isArchived() { + return archived; + } + + public void setArchived(boolean archived) { + this.archived = archived; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index e76ce67..28eb1cf 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -87,36 +87,80 @@ public class ClientHandler implements Runnable { List groups = GroupDatabase.getGroupsByUser(user.getInternal_uuid()); List channels = ChannelDatabase.getChannelsByUser(user.getInternal_uuid()); List unreadMessages = MessageDatabase.getUnreadMessages(user.getInternal_uuid()); + List archivedChatIds = ArchivedChatDatabase.getArchivedChats(user.getInternal_uuid()); + + user.setContactList(contacts); user.setGroupList(groups); user.setChannelList(channels); user.setUnreadMessages(unreadMessages); + + List chatList = new ArrayList<>(); + List archivedChatList = new ArrayList<>(); + List activeChatList = new ArrayList<>(); + + for (Contact contact : contacts) { User target = userDatabase.findByInternalUUID(contact.getContact_id()); if (target == null) continue; LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(user.getInternal_uuid(), target.getInternal_uuid(), "private"); - chatList.add(new ChatEntry( - target.getInternal_uuid(), // internal UUID - target.getUser_id(), // public display ID +// chatList.add(new ChatEntry( +// target.getInternal_uuid(), // internal UUID +// target.getUser_id(), // public display ID +// target.getProfile_name(), +// target.getImage_url(), +// "private", +// last, +// false, +// false +// )); + + UUID targetId = target.getInternal_uuid(); + + ChatEntry entry = new ChatEntry( + targetId, + target.getUser_id(), target.getProfile_name(), target.getImage_url(), "private", last, false, false - )); + ); + + + + if (archivedChatIds.contains(targetId)) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + for (Group group : groups) { LocalDateTime last = MessageDatabase.getLastMessageTime(group.getInternal_uuid(), "group"); boolean isOwner = GroupDatabase.isOwner(group.getInternal_uuid(), user.getInternal_uuid()); boolean isAdmin = GroupDatabase.isAdmin(group.getInternal_uuid(), user.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// group.getInternal_uuid(), +// group.getGroup_id(), +// group.getGroup_name(), +// group.getImage_url(), +// "group", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( group.getInternal_uuid(), group.getGroup_id(), group.getGroup_name(), @@ -125,7 +169,17 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(group.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } + + } for (Channel channel : channels) { @@ -133,7 +187,18 @@ public class ClientHandler implements Runnable { boolean isOwner = ChannelDatabase.isOwner(channel.getInternal_uuid(), user.getInternal_uuid()); boolean isAdmin = ChannelDatabase.isAdmin(channel.getInternal_uuid(), user.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// channel.getInternal_uuid(), +// channel.getChannel_id(), +// channel.getChannel_name(), +// channel.getImage_url(), +// "channel", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( channel.getInternal_uuid(), channel.getChannel_id(), channel.getChannel_name(), @@ -142,9 +207,29 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(channel.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + activeChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + + archivedChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + chatList.sort((a, b) -> { if (a.getLastMessageTime() == null) return 1; @@ -154,6 +239,8 @@ public class ClientHandler implements Runnable { JSONObject userData = JsonUtil.userToJson(user); userData.put("chat_list", JsonUtil.chatListToJson(chatList)); + userData.put("archived_chat_list", JsonUtil.chatListToJson(archivedChatList)); + userData.put("active_chat_lis", JsonUtil.chatListToJson(activeChatList)); response = new ResponseModel("success", "Welcome " + user.getProfile_name(), userData); } break; @@ -1708,6 +1795,42 @@ public class ClientHandler implements Runnable { // } // + case "archive_chat": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + String chatType = requestJson.getString("chat_type"); + + boolean success = ArchivedChatDatabase.archiveChat(currentUser.getInternal_uuid(), chatId, chatType); + response = success + ? new ResponseModel("success", "Chat archived successfully.") + : new ResponseModel("error", "Failed to archive chat."); + break; + } + + + case "unarchive_chat": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + + boolean success = ArchivedChatDatabase.unarchiveChat(currentUser.getInternal_uuid(), chatId); + response = success + ? new ResponseModel("success", "Chat unarchived successfully.") + : new ResponseModel("error", "Failed to unarchive chat."); + break; + } + + + + + default: From 5c6a63c6ae2d2e93b37b92433df83cf398bb5e59 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Tue, 29 Jul 2025 13:00:19 +0330 Subject: [PATCH 03/11] Update init.sql --- src/main/resources/init.sql | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index f07532e..981936b 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -27,15 +27,15 @@ CREATE TABLE IF NOT EXISTS messages ( receiver_id UUID NOT NULL , content TEXT, message_type VARCHAR(20) DEFAULT 'TEXT' CHECK (message_type IN ('TEXT','IMAGE','FILE','VIDEO','AUDIO','STICKER','GIF')), - file_url TEXT, --(Bouns) send_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20), -- SEND,DELIVERED,READ reply_to_id UUID REFERENCES messages(message_id) ON DELETE SET NULL, --(Bouns) - is_edited BOOLEAN DEFAULT FALSE, --(Bonus) - original_message_id UUID REFERENCES messages(message_id) - ON DELETE SET NULL, --(Bonus) + is_edited BOOLEAN DEFAULT FALSE, --(Bonus) + edited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + original_message_id UUID REFERENCES messages(message_id) ON DELETE SET NULL, --(Bonus) forwarded_by UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, --(Bouns) - forwarded_from UUID REFERENCES users(internal_uuid) ON DELETE SET NULL --(Bouns) + forwarded_from UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, --(Bouns) + is_deleted_globally BOOLEAN DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS private_chat ( @@ -119,3 +119,30 @@ CREATE TABLE archived_chats ( ); +CREATE TABLE deleted_messages ( + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(internal_uuid) ON DELETE CASCADE, + deleted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (message_id, user_id) +); + + +CREATE TABLE IF NOT EXISTS message_reactions ( + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + user_id UUID REFERENCES users(internal_uuid) ON DELETE CASCADE, + emoji TEXT NOT NULL, + reacted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (message_id, user_id) +); + +--Multi attachments +CREATE TABLE IF NOT EXISTS message_attachments ( + attachment_id UUID PRIMARY KEY, + message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE, + file_url TEXT NOT NULL, + file_type VARCHAR(20) CHECK (file_type IN ('IMAGE','VIDEO','AUDIO','FILE','GIF','STICKER')), + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + + + From c6eea669665a4ba1dc91e28a97666218429a8b78 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 30 Jul 2025 18:00:56 +0330 Subject: [PATCH 04/11] Send text message --- .../Client/ActionHandler.java | 100 ++++++++++++------ .../Client/IncomingMessageListener.java | 69 +++++++++--- .../Database/MessageDatabase.java | 95 ++++++++++++----- .../Database/PrivateChatDatabase.java | 32 ++++++ .../Models/ChatEntry.java | 2 + .../Models/FileAttachment.java | 19 ++++ .../telegramfinalproject/Models/JsonUtil.java | 1 - .../telegramfinalproject/Models/Message.java | 31 ++++-- .../Server/ClientHandler.java | 91 ++++++++++++++++ .../Server/RealTimeEventDispatcher.java | 28 ++++- 10 files changed, 386 insertions(+), 82 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java create mode 100644 src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 6173d93..0bb8ce1 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -25,6 +25,8 @@ public class ActionHandler { public static ActionHandler instance; + + private void handleRealTime(JSONObject json) throws IOException { IncomingMessageListener listener = new IncomingMessageListener(this.in); listener.handleRealTimeEvent (json); @@ -705,12 +707,14 @@ public class ActionHandler { System.out.println("\n🔓 Messages fetched:"); System.out.println("─────────────────────────────────────────────"); for (int i = 0; i < messages.length(); i++) { + JSONObject m = messages.getJSONObject(i); String senderId = m.getString("sender_id"); + String senderName = m.optString("sender_name", "Other"); String content = m.getString("content"); String time = m.getString("send_at"); - String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : "Other"; + String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; System.out.println("[" + time + "] " + label + ": " + content); } System.out.println("─────────────────────────────────────────────"); @@ -891,10 +895,11 @@ public class ActionHandler { for (int i = 0; i < messages.length(); i++) { JSONObject m = messages.getJSONObject(i); String senderId = m.getString("sender_id"); + String senderName = m.optString("sender_name", "Other"); String content = m.getString("content"); String time = m.getString("send_at"); - String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : "Other"; + String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; System.out.println("[" + time + "] " + label + ": " + content); } System.out.println("─────────────────────────────────────────────"); @@ -982,7 +987,7 @@ public class ActionHandler { String input = scanner.nextLine(); switch (input) { - case "1" -> sendMessageTo(chat.getId(), "private"); + case "1" -> sendMessage(chat.getId(), "private"); case "2" -> toggleBlock(chat.getId()); case "3" -> { deleteChat(chat.getId(), false); @@ -1094,7 +1099,7 @@ public class ActionHandler { String input = scanner.nextLine(); switch (input) { - case "1" -> sendMessageTo(chat.getId(), "group"); + case "1" -> sendMessage(chat.getId(), "group"); case "2" -> viewGroupMembers(chat.getId()); case "3" -> { if (isOwner || (isAdmin && perms.optBoolean("can_add_members", false))) @@ -1216,7 +1221,7 @@ public class ActionHandler { switch (input) { case "1" -> { if (isOwner || (isAdmin && perms.optBoolean("can_post", false))) { - sendMessageTo(chat.getId(), "channel"); + sendMessage(chat.getId(), "channel"); } else { System.out.println("❌ You don't have permission to post."); } @@ -1985,31 +1990,7 @@ public class ActionHandler { - private void sendMessageTo(UUID id, String type) { - System.out.print("Enter message: "); - String text = scanner.nextLine().trim(); - if (text.isEmpty()) { - System.out.println("Message cannot be empty."); - return; - } - - JSONObject req = new JSONObject(); - req.put("action", "send_message"); - req.put("sender_id", Session.getUserUUID()); - req.put("receiver_id", id.toString()); - req.put("receiver_type", type); - req.put("text", text); - - JSONObject res = sendWithResponse(req); - if (res == null) return; - - if (res.getBoolean("success")) { - System.out.println("✅ Message sent."); - } else { - System.out.println("❌ Failed to send message."); - } - } private void addMemberToGroup(UUID groupId, UUID userId) { JSONObject req = new JSONObject(); @@ -2529,7 +2510,8 @@ public class ActionHandler { System.out.println("\n💬 Your Chats:"); int index = 1; - for (ChatEntry chat : Session.chatList) { + System.out.println("0. Archived chats"); + for (ChatEntry chat : Session.activeChats) { System.out.printf("%d. [%s] %s (%s)\n", index++, chat.getType(), chat.getName(), chat.getDisplayId()); } } @@ -2898,6 +2880,64 @@ public class ActionHandler { } + public void sendMessage(UUID receiverId, String receiverType) { + Scanner scanner = new Scanner(System.in); + + System.out.print("Enter your message: "); + String content = scanner.nextLine(); + + System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); + String messageType = scanner.nextLine(); + Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); + while (!allowedTypes.contains(messageType.toUpperCase())) { + System.out.println("❌ Invalid message type. Try again (TEXT / IMAGE / VIDEO / FILE): "); + messageType = scanner.nextLine(); + } + + JSONArray attachmentsArray = new JSONArray(); + + System.out.print("Do you want to attach files? (yes/no): "); + if (scanner.nextLine().equalsIgnoreCase("yes")) { + while (true) { + System.out.print("File URL: "); + String fileUrl = scanner.nextLine(); + + System.out.print("File Type (IMAGE / VIDEO / FILE): "); + String fileType = scanner.nextLine(); + + JSONObject fileJson = new JSONObject(); + fileJson.put("file_url", fileUrl); + fileJson.put("file_type", fileType); + attachmentsArray.put(fileJson); + + System.out.print("Add another file? (yes/no): "); + if (!scanner.nextLine().equalsIgnoreCase("yes")) { + break; + } + } + } + + JSONObject messageJson = new JSONObject(); + messageJson.put("action", "send_message"); + messageJson.put("receiver_id", receiverId.toString()); + messageJson.put("receiver_type", receiverType); + messageJson.put("content", content); + messageJson.put("message_type", messageType); + + if (!attachmentsArray.isEmpty()) { + messageJson.put("attachments", attachmentsArray); + } + + JSONObject response = sendWithResponse(messageJson); + if (response != null && response.getString("status").equals("success")) { + System.out.println("✅ Message sent successfully! ID: " + response.getJSONObject("data").getString("message_id")); + } else { + System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "no response")); + } + + } + + diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index e10e03e..c0e8180 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -107,16 +107,22 @@ public class IncomingMessageListener implements Runnable { } case "chat_updated" -> { - System.out.println("\n🔄 Group/Channel info updated."); - new Thread(() -> { - try { - handleAdminRoleChanged(msg); - } catch (IOException e) { - e.printStackTrace(); - } - }).start(); + System.out.println("\n🔄 Chat info updated."); + + if (msg.has("last_message_time")) { + updateLastMessageTime(msg); + } else { + new Thread(() -> { + try { + handleAdminRoleChanged(msg); + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + } } + case "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> { System.out.println("🧩 Detected admin/owner role change. Calling handler..."); new Thread(() -> { @@ -139,6 +145,30 @@ public class IncomingMessageListener implements Runnable { System.out.print(">> "); } + private void updateLastMessageTime(JSONObject msg) { + try { + UUID chatUUID = UUID.fromString(msg.getString("chat_id")); + String newTime = msg.optString("last_message_time", null); + + Session.chatList.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + if (Session.inChatListMenu) { + ActionHandler.displayChatList(); + System.out.print("Select a chat by number: "); + } + + } catch (Exception e) { + System.out.println("❌ Failed to update last message time: " + e.getMessage()); + } + } + + private void handleAdminRoleChanged(JSONObject data) throws IOException { String chatType = data.getString("chat_type"); String chatId = data.optString("group_id", data.optString("channel_id", data.optString("chat_id", null))); @@ -230,11 +260,26 @@ public class IncomingMessageListener implements Runnable { private void displayRealTimeMessage(String action, JSONObject msg) { switch (action) { case "new_message" -> { - System.out.println("\n🔔 New Message:"); - System.out.println("From: " + msg.getString("sender")); - System.out.println("Time: " + msg.getString("time")); - System.out.println("Content: " + msg.getString("content")); + 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 receiverId = msg.optString("receiver_id", ""); + String receiverType = msg.optString("receiver_type", ""); + + boolean isInCurrentChat = Session.inChatMenu && + Session.currentChatId != null && + Session.currentChatId.equals(receiverId); + + if (isInCurrentChat) { + System.out.println(senderName + ": " + content + " (" + sendAt + ")"); + } else { + System.out.println("💬 Message from " + senderName + " in " + receiverType + " chat: " + content); + Session.forceRefreshChatList = true; + } } + case "message_edited" -> { System.out.println("\n✏️ Message Edited:"); System.out.println("ID: " + msg.getString("message_id")); diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 22ef33b..54d307a 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -1,5 +1,6 @@ package org.to.telegramfinalproject.Database; +import org.to.telegramfinalproject.Models.FileAttachment; import org.to.telegramfinalproject.Models.Message; import java.sql.*; @@ -12,38 +13,55 @@ import java.util.stream.Collectors; public class MessageDatabase { - public static void save(Message message) { - String sql = """ - INSERT INTO messages ( - message_id, sender_id, receiver_type, receiver_id, content, - message_type, file_url, send_at, status, - reply_to_id, is_edited, original_message_id, forwarded_by, forwarded_from - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """; + + + + public static boolean insertMessage(UUID messageId, UUID senderId, UUID receiverId, + String receiverType, String content, String messageType) { + String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type) " + + "VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = ConnectionDb.connect(); - PreparedStatement stmt = conn.prepareStatement(sql)) { + PreparedStatement ps = conn.prepareStatement(sql)) { - stmt.setObject(1, message.getMessage_id()); - stmt.setObject(2, message.getSender_id()); - stmt.setString(3, message.getReceiver_type()); - stmt.setObject(4, message.getReceiver_id()); - stmt.setString(5, message.getContent()); - stmt.setString(6, message.getMessage_type()); - stmt.setString(7, message.getFile_url()); - stmt.setObject(8, message.getSend_at()); - stmt.setString(9, message.getStatus()); - stmt.setObject(10, message.getReply_to_id()); - stmt.setBoolean(11, message.isIs_edited()); - stmt.setObject(12, message.getOriginal_message_id()); - stmt.setObject(13, message.getForwarded_by()); - stmt.setObject(14, message.getForwarded_from()); + ps.setObject(1, messageId); + ps.setObject(2, senderId); + ps.setString(3, receiverType); + ps.setObject(4, receiverId); + ps.setString(5, content); + ps.setString(6, messageType); - stmt.executeUpdate(); + return ps.executeUpdate() > 0; } catch (SQLException e) { - System.err.println("❌ Error saving message: " + e.getMessage()); e.printStackTrace(); + return false; + } + } + + + + public static boolean insertAttachments(UUID messageId, List attachments) { + String sql = "INSERT INTO message_attachments (attachment_id, message_id, file_url, file_type) " + + "VALUES (?, ?, ?, ?)"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + for (FileAttachment att : attachments) { + ps.setObject(1, UUID.randomUUID()); + ps.setObject(2, messageId); + ps.setString(3, att.getFileUrl()); + ps.setString(4, att.getFileType()); + ps.addBatch(); + } + + ps.executeBatch(); + return true; + + } catch (SQLException e) { + e.printStackTrace(); + return false; } } @@ -100,7 +118,6 @@ public class MessageDatabase { UUID.fromString(rs.getString("receiver_id")), rs.getString("content"), rs.getString("message_type"), - rs.getString("file_url"), rs.getTimestamp("send_at").toLocalDateTime(), rs.getString("status"), rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null, @@ -118,6 +135,31 @@ 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 = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setObject(1, messageId); + ResultSet rs = stmt.executeQuery(); + + while (rs.next()) { + attachments.add(new FileAttachment( + rs.getString("file_url"), + rs.getString("file_type") + )); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return attachments; + } + + public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) { String sql = """ @@ -177,7 +219,6 @@ public class MessageDatabase { UUID.fromString(rs.getString("receiver_id")), rs.getString("content"), rs.getString("message_type"), - rs.getString("file_url"), rs.getTimestamp("send_at").toLocalDateTime(), rs.getString("status"), (UUID) rs.getObject("reply_to_id"), diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java new file mode 100644 index 0000000..e13a2e5 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -0,0 +1,32 @@ +package org.to.telegramfinalproject.Database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +public class PrivateChatDatabase { + + + public static List getMembers(UUID privateChatId) { + String sql = "SELECT user1, user2 FROM private_chats WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, privateChatId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + UUID user1 = (UUID) rs.getObject("user1"); + UUID user2 = (UUID) rs.getObject("user2"); + return Arrays.asList(user1, user2); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return new ArrayList<>(); + } + +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index b634ce3..6060f0d 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -115,4 +115,6 @@ public class ChatEntry { this.archived = archived; } + public void setLastMessageTime(String newTime) {this.lastMessageTime = LocalDateTime.parse(newTime); + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java new file mode 100644 index 0000000..430e457 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java @@ -0,0 +1,19 @@ +package org.to.telegramfinalproject.Models; + +public class FileAttachment { + private String fileUrl; + private String fileType; + + public FileAttachment(String fileUrl, String fileType) { + this.fileUrl = fileUrl; + this.fileType = fileType; + } + + public String getFileUrl() { + return fileUrl; + } + + public String getFileType() { + return fileType; + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java index a9ed199..1b97b58 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java @@ -54,7 +54,6 @@ public class JsonUtil { obj.put("receiver_id", message.getReceiver_id().toString()); obj.put("content", message.getContent()); obj.put("message_type", message.getMessage_type()); - obj.put("file_url", message.getFile_url()); obj.put("send_at", message.getSend_at().toString()); obj.put("status", message.getStatus()); obj.put("reply_to_id", message.getReply_to_id() != null ? message.getReply_to_id().toString() : JSONObject.NULL); diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index eb7688a..a3de400 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -1,6 +1,7 @@ package org.to.telegramfinalproject.Models; import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; public class Message { @@ -10,7 +11,6 @@ public class Message { private UUID receiver_id; private String content; private String message_type; // TEXT, IMAGE, FILE, ... - private String file_url; private LocalDateTime send_at; private String status; // SENT, DELIVERED, READ private UUID reply_to_id; @@ -18,9 +18,10 @@ public class Message { private UUID original_message_id; private UUID forwarded_by; private UUID forwarded_from; + private List attachments; public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content, - String message_type, String file_url, LocalDateTime send_at, String status, + 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) { this.message_id = message_id; @@ -29,7 +30,6 @@ public class Message { this.receiver_id = receiver_id; this.content = content; this.message_type = message_type; - this.file_url = file_url; this.send_at = send_at; this.status = status; this.reply_to_id = reply_to_id; @@ -39,6 +39,16 @@ public class Message { this.forwarded_from = forwarded_from; } + public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType, String content, String messageType, LocalDateTime now) { + 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; + } + public UUID getMessage_id() { return message_id; @@ -85,14 +95,6 @@ public class Message { this.message_type = message_type; } - public String getFile_url() { - return file_url; - } - - public void setFile_url(String file_url) { - this.file_url = file_url; - } - public LocalDateTime getSend_at() { return send_at; } @@ -148,4 +150,11 @@ public class Message { 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 28eb1cf..78e0aa8 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1157,6 +1157,9 @@ public class ClientHandler implements Runnable { obj.put("receiver_type", m.getReceiver_type()); obj.put("content", m.getContent()); obj.put("send_at", m.getSend_at().toString()); + User senderUser = userDatabase.findByInternalUUID(m.getSender_id()); + String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown"; + obj.put("sender_name", senderName); messageArray.put(obj); } @@ -1827,6 +1830,12 @@ public class ClientHandler implements Runnable { break; } + case "send_message" : { + response = handleSendMessage(requestJson); + } + break; + + @@ -1888,4 +1897,86 @@ public class ClientHandler implements Runnable { } + private ResponseModel handleSendMessage(JSONObject json) { + try { + if (currentUser == null) + return new ResponseModel("error", "Unauthorized. Please login first."); + + UUID messageId = UUID.randomUUID(); + UUID senderId = currentUser.getInternal_uuid(); + UUID receiverId = UUID.fromString(json.getString("receiver_id")); + String receiverType = json.getString("receiver_type"); + String content = json.optString("content", ""); + String messageType = json.optString("message_type", "TEXT"); + + boolean inserted = MessageDatabase.insertMessage(messageId, senderId, receiverId, receiverType, content, messageType); + if (!inserted) + return new ResponseModel("error", "Failed to insert message."); + + if (json.has("attachments")) { + JSONArray attachmentsArray = json.getJSONArray("attachments"); + List attachments = new ArrayList<>(); + + for (int i = 0; i < attachmentsArray.length(); i++) { + JSONObject attJson = attachmentsArray.getJSONObject(i); + attachments.add(new FileAttachment( + attJson.getString("file_url"), + attJson.getString("file_type") + )); + } + + boolean attInserted = MessageDatabase.insertAttachments(messageId, attachments); + if (!attInserted) + return new ResponseModel("error", "Message inserted but failed to attach files."); + } + + // ری‌ل‌تایم (اختیاری: dispatchMessageToReceiver(messageId, receiverId, receiverType)) + + + Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now()); + List receivers = getReceiversForChat(receiverId, receiverType); + receivers.remove(senderId); + + RealTimeEventDispatcher.sendNewMessage(msg, receivers); + + + JSONObject chatUpdate = new JSONObject(); + chatUpdate.put("chat_id", receiverId.toString()); + chatUpdate.put("chat_type", receiverType); + chatUpdate.put("last_message_time", LocalDateTime.now().toString()); + + JSONObject chatPayload = new JSONObject(); + chatPayload.put("action", "chat_updated"); + chatPayload.put("data", chatUpdate); + + for (UUID receiver : receivers) { + RealTimeEventDispatcher.sendToUser(receiver, chatPayload); + } + + + JSONObject data = new JSONObject(); + data.put("message_id", messageId.toString()); + return new ResponseModel("success", "Message sent successfully.", data); + + } catch (Exception e) { + e.printStackTrace(); + return new ResponseModel("error", "Exception occurred while sending message."); + } + } + + private List getReceiversForChat(UUID receiverId, String receiverType) { + switch (receiverType) { + case "private": + return PrivateChatDatabase.getMembers(receiverId); + case "group": + return GroupDatabase.getMemberUUIDs(receiverId); + case "channel": + return ChannelDatabase.getSubscriberUUIDs(receiverId); + default: + return new ArrayList<>(); + } + } + + + } \ 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 3c96120..c9de28d 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -3,6 +3,7 @@ package org.to.telegramfinalproject.Server; import org.json.JSONObject; import org.to.telegramfinalproject.Database.ChannelDatabase; import org.to.telegramfinalproject.Database.GroupDatabase; +import org.to.telegramfinalproject.Database.userDatabase; import org.to.telegramfinalproject.Models.Message; import org.to.telegramfinalproject.Models.User; @@ -151,7 +152,6 @@ public class RealTimeEventDispatcher { data.put("sender", sender.getUser_id()); data.put("receiver_type", msg.getReceiver_type()); data.put("receiver_id", msg.getReceiver_id()); - data.put("file_url", msg.getFile_url()); data.put("file_type", msg.getMessage_type()); // IMAGE, FILE, VIDEO... data.put("time", msg.getSend_at().toString()); @@ -305,4 +305,30 @@ public class RealTimeEventDispatcher { broadcastToUsers(affectedUsers, event); } + + public static void sendNewMessage(Message message, List receivers) { + JSONObject payload = new JSONObject(); + payload.put("action", "new_message"); + + JSONObject data = new JSONObject(); + data.put("id", message.getMessage_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("send_at", message.getSend_at().toString()); + + User sender = userDatabase.findByInternalUUID(message.getSender_id()); + if (sender != null) { + data.put("sender_name", sender.getProfile_name()); + } + + payload.put("data", data); + + for (UUID userId : receivers) { + sendToUser(userId, payload); + } + } + + } From f604ca4f2247195d1d687090410519363ca50a43 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 30 Jul 2025 21:10:33 +0330 Subject: [PATCH 05/11] Send text message Real time updated --- .../Client/ActionHandler.java | 12 ++++- .../Client/IncomingMessageListener.java | 38 +++++++++++++++ .../telegramfinalproject/Client/Session.java | 46 ++++++++++++++++++- .../Models/ChatEntry.java | 18 +++++++- .../Server/ClientHandler.java | 1 - 5 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 0bb8ce1..4d5ee15 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -2508,14 +2508,22 @@ public class ActionHandler { return; } + System.out.println("\n💬 Your Chats:"); + System.out.println("0. 📦 Archived chats"); + int index = 1; - System.out.println("0. Archived chats"); for (ChatEntry chat : Session.activeChats) { - System.out.printf("%d. [%s] %s (%s)\n", index++, chat.getType(), chat.getName(), chat.getDisplayId()); + String time = (chat.getLastMessageTime() == null) + ? "No messages yet" + : chat.getLastMessageTime().toString(); + + System.out.printf("%d. [%s] %s (%s) - Last: %s\n", + index++, chat.getType(), chat.getName(), chat.getDisplayId(), time); } } + public static class ChatStateMonitor implements Runnable { private final PrintWriter out; diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index c0e8180..5168ba9 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -158,6 +158,44 @@ public class IncomingMessageListener implements Runnable { System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); }); + Session.activeChats.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.archivedChats.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.chatList.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + Session.activeChats.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + Session.archivedChats.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + + + + if (Session.inChatListMenu) { ActionHandler.displayChatList(); System.out.print("Select a chat by number: "); diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index c49bb3d..11b11d9 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -4,6 +4,7 @@ import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Models.ChatEntry; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -36,24 +37,65 @@ public class Session { throw new RuntimeException("❌ No UUID found in currentUser!"); } +// public static void updateChatList(JSONArray chatArray) { +// chatList.clear(); +// for (int i = 0; i < chatArray.length(); i++) { +// JSONObject obj = chatArray.getJSONObject(i); +// ChatEntry entry = new ChatEntry( +// UUID.fromString(obj.getString("internal_id")), +// obj.optString("id", ""), // displayId +// obj.optString("name", ""), // name +// obj.optString("image_url", ""), +// obj.getString("type"), +// null, // last message time (if needed, parse it) +// obj.optBoolean("is_owner", false), +// obj.optBoolean("is_admin", false) +// ); +// entry.setPermissions(obj.optJSONObject("permissions")); +// chatList.add(entry); +// } +// } + + public static void updateChatList(JSONArray chatArray) { chatList.clear(); for (int i = 0; i < chatArray.length(); i++) { JSONObject obj = chatArray.getJSONObject(i); + + LocalDateTime lastMessageTime = null; + if (obj.has("last_message_time") && !obj.isNull("last_message_time")) { + String timeStr = obj.getString("last_message_time"); + if (!timeStr.isBlank()) { + lastMessageTime = LocalDateTime.parse(timeStr); + } + } + ChatEntry entry = new ChatEntry( UUID.fromString(obj.getString("internal_id")), obj.optString("id", ""), // displayId obj.optString("name", ""), // name obj.optString("image_url", ""), obj.getString("type"), - null, // last message time (if needed, parse it) + lastMessageTime, obj.optBoolean("is_owner", false), obj.optBoolean("is_admin", false) ); - entry.setPermissions(obj.optJSONObject("permissions")); // اگر permissions وجود داره + + entry.setPermissions(obj.optJSONObject("permissions")); chatList.add(entry); } + + chatList.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); + }); + + activeChats = chatList.stream().filter(c -> !c.isArchived()).toList(); + archivedChats = chatList.stream().filter(ChatEntry::isArchived).toList(); } + public static List getChatList() { return chatList; } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index 6060f0d..aa98d0b 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -115,6 +115,22 @@ public class ChatEntry { this.archived = archived; } - public void setLastMessageTime(String newTime) {this.lastMessageTime = LocalDateTime.parse(newTime); +// public void setLastMessageTime(String newTime) {this.lastMessageTime = LocalDateTime.parse(newTime); +// } + + + public void setLastMessageTime(String newTime) { + if (newTime == null || newTime.isBlank()) { + this.lastMessageTime = null; + return; + } + + try { + this.lastMessageTime = LocalDateTime.parse(newTime); + } catch (Exception e) { + System.out.println("❌ Failed to parse lastMessageTime: " + newTime); + this.lastMessageTime = null; + } } + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 78e0aa8..9596845 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1930,7 +1930,6 @@ public class ClientHandler implements Runnable { return new ResponseModel("error", "Message inserted but failed to attach files."); } - // ری‌ل‌تایم (اختیاری: dispatchMessageToReceiver(messageId, receiverId, receiverType)) Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now()); From a2816972d1a7e9529b533b561e23ba484840c64b Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 1 Aug 2025 14:10:09 +0330 Subject: [PATCH 06/11] fix open private chats --- .../Client/ActionHandler.java | 308 ++++++++++++++++-- .../telegramfinalproject/Client/Session.java | 5 +- .../Database/MessageDatabase.java | 38 +++ .../Database/PrivateChatDatabase.java | 112 ++++++- .../Models/ChatEntry.java | 7 + .../Models/ContactEntry.java | 44 +++ .../Models/PrivateChat.java | 2 +- .../Server/ClientHandler.java | 267 ++++++++++++--- 8 files changed, 718 insertions(+), 65 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 4d5ee15..ddfd379 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -2,7 +2,9 @@ package org.to.telegramfinalproject.Client; import org.json.JSONArray; import org.json.JSONObject; +import org.to.telegramfinalproject.Database.PrivateChatDatabase; import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; import org.to.telegramfinalproject.Models.SearchResultModel; @@ -443,7 +445,8 @@ public class ActionHandler { JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); JSONArray Archived = Session.currentUser.getJSONArray("archived_chat_list"); - JSONArray Active = Session.currentUser.getJSONArray("active_chat_lis"); + JSONArray Active = Session.currentUser.getJSONArray("active_chat_list"); + JSONArray contactList = Session.currentUser.getJSONArray("contact_list"); List chatList = new ArrayList<>(); @@ -459,9 +462,13 @@ public class ActionHandler { 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")) { + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); + } chatList.add(entry); @@ -481,6 +488,9 @@ public class ActionHandler { chat.optBoolean("is_owner", false), chat.optBoolean("is_admin", false) ); + if (chat.has("other_user_id")) { + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat + } archivedChats.add(entry); } @@ -499,11 +509,28 @@ public class ActionHandler { chat.optBoolean("is_owner", false), chat.optBoolean("is_admin", false) ); + if (chat.has("other_user_id")) { + entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat + } activeChats.add(entry); } + Session.contactEntries.clear(); + for (Object obj : contactList) { + 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) + ); + Session.contactEntries.add(entry); + } + @@ -751,7 +778,8 @@ public class ActionHandler { System.out.println("2. Search"); System.out.println("3. Create Channel"); System.out.println("4. Create group"); - System.out.println("5. Logout"); + System.out.println("5. View contacts"); + System.out.println("6. Logout"); System.out.print("Choose an option: "); String choice = scanner.nextLine(); @@ -764,7 +792,8 @@ public class ActionHandler { case "2" -> search(); case "3" -> createChannel(); case "4" -> createGroup(); - case "5" -> { + case "5" -> showContactList(); + case "6" -> { logout(); return; } @@ -773,6 +802,109 @@ public class ActionHandler { } } + public void showContactList() { + List contacts = Session.contactEntries; + if (contacts.isEmpty()) { + System.out.println("📭 You have no contacts."); + return; + } + + System.out.println("👥 Your Contacts:"); + for (int i = 0; i < contacts.size(); i++) { + System.out.println((i + 1) + ". " + contacts.get(i)); + } + + System.out.print("Select a contact (0 to go back): "); + int choice = scanner.nextInt(); + scanner.nextLine(); + + if (choice == 0) return; + if (choice < 1 || choice > contacts.size()) { + System.out.println("❌ Invalid choice."); + return; + } + + ContactEntry selected = contacts.get(choice - 1); + System.out.println("\n📇 What do you want to do with " + selected.getProfileName() + "?"); + System.out.println("1. View Profile"); + System.out.println("2. Send Message"); + System.out.print("Enter your choice: "); + int action = scanner.nextInt(); + scanner.nextLine(); + + switch (action) { + case 1 -> viewProfile(selected.getContactId()); + case 2 -> startPrivateChat(selected); + default -> System.out.println("❌ Invalid option."); + } + } + + private void viewProfile(UUID targetId) { + JSONObject req = new JSONObject(); + req.put("action", "view_profile"); + req.put("target_id", targetId.toString()); + + JSONObject res = sendWithResponse(req); + if (res == null || !res.getString("status").equals("success")) { + System.out.println("❌ Failed to fetch profile."); + return; + } + + JSONObject data = res.getJSONObject("data"); + String profileName = data.getString("profile_name"); + String userId = data.getString("user_id"); + String bio = data.optString("bio", "(no bio)"); + String imageUrl = data.optString("image_url", "(no image)"); + boolean isOnline = data.getBoolean("is_online"); + String lastSeen = data.getString("last_seen"); + + System.out.println("\n📄 Profile Info:"); + System.out.println("Name: " + profileName); + System.out.println("User ID: " + userId); + System.out.println("Bio: " + bio); + System.out.println("Image: " + imageUrl); + System.out.println("Status: " + (isOnline ? "🟢 Online" : "🔘 Last seen at " + lastSeen)); + } + + + private void startPrivateChat(ContactEntry contact) { + UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid")); + UUID contactId = contact.getContactId(); + + JSONObject req = new JSONObject(); + req.put("action", "get_or_create_private_chat"); + req.put("user1", myId.toString()); + req.put("user2", contactId.toString()); + + JSONObject res = sendWithResponse(req); + if (res == null || !res.getString("status").equals("success")) { + System.out.println("❌ Failed to create or fetch private chat."); + return; + } + + JSONObject data = res.getJSONObject("data"); + UUID chatId = UUID.fromString(data.getString("chat_id")); + + ChatEntry entry = new ChatEntry( + chatId, + contact.getUserId(), + contact.getProfileName(), + contact.getImageUrl(), + "private", + null, + false, + false + ); + entry.setOtherUserId(contactId); + + Session.chatList.add(0, entry); // اضافه به اول لیست + System.out.println("✅ Chat with " + contact.getProfileName() + " started."); + + openChat(entry); // 👈 مستقیم وارد چت شو (اختیاری) + } + + + // public void showChatListAndSelect() { // // @@ -878,10 +1010,54 @@ public class ActionHandler { private void openChat(ChatEntry chat) { + + //for private chats only + if (chat.getType().equalsIgnoreCase("private")) { + JSONObject reqTarget = new JSONObject(); + reqTarget.put("action", "get_private_chat_target"); + reqTarget.put("chat_id", chat.getId()); + + JSONObject resTarget = sendWithResponse(reqTarget); + if (resTarget == null || !resTarget.getString("status").equals("success")) { + System.out.println("❌ Failed to fetch target user for private chat."); + return; + } + String otherUserId = resTarget.getJSONObject("data").getString("target_id"); + chat.setOtherUserId(UUID.fromString(otherUserId)); + } + + + JSONObject req = new JSONObject(); req.put("action", "get_messages"); - req.put("receiver_id", chat.getId()); + req.put("receiver_id",chat.getId()); req.put("receiver_type", chat.getType()); +// String type = chat.getType(); +// UUID chatId = chat.getId(); +// if (type.equals("private")) { +// UUID myId = UUID.fromString(Session.currentUser.getString("internal_uuid")); +// UUID otherUserId = chat.getId(); +// +// JSONObject getChatIdReq = new JSONObject(); +// getChatIdReq.put("action", "get_or_create_private_chat"); +// getChatIdReq.put("user1", myId.toString()); +// getChatIdReq.put("user2", otherUserId.toString()); +// +// JSONObject chatIdRes = sendWithResponse(getChatIdReq); +// if (chatIdRes == null || !chatIdRes.getString("status").equals("success")) { +// System.out.println("❌ Failed to fetch private chat ID."); +// return; +// } +// +// Session.currentPrivateChatUserId = otherUserId; +// chatId = UUID.fromString(chatIdRes.getJSONObject("data").getString("chat_id")); +// chat.setId(String.valueOf(chatId)); +// +// } + + + + JSONObject res = sendWithResponse(req); if (res == null || !res.getString("status").equals("success")) { @@ -904,6 +1080,15 @@ public class ActionHandler { } System.out.println("─────────────────────────────────────────────"); + if (chat.getType().equals("private") && chat.getOtherUserId() == null) { + for (ContactEntry contact : Session.contactEntries) { + if (contact.getContactId().equals(chat.getId())) { + chat.setOtherUserId(contact.getContactId()); + break; + } + } + } + boolean stayInChat = true; @@ -957,7 +1142,7 @@ public class ActionHandler { JSONObject req = new JSONObject(); req.put("action", "view_profile"); - req.put("target_id", chat.getId()); // internal UUID + req.put("target_id", chat.getOtherUserId()); // internal UUID JSONObject res = sendWithResponse(req); @@ -1000,7 +1185,7 @@ public class ActionHandler { case "5" -> { JSONObject reqProfile = new JSONObject(); reqProfile.put("action", "view_profile"); - reqProfile.put("target_id", chat.getId()); + reqProfile.put("target_id", chat.getOtherUserId()); JSONObject resProfile = sendWithResponse(reqProfile); @@ -2445,6 +2630,9 @@ public class ActionHandler { System.err.println("❌ Invalid request: missing action."); return null; } + System.out.println("📤 Sending request to server: " + request.toString(2)); + System.out.println("📤 [sendWithResponse] Action: " + request.optString("action", "unknown") + ", Full: " + request.toString(2)); + String requestId = UUID.randomUUID().toString(); request.put("request_id", requestId); @@ -2888,6 +3076,72 @@ public class ActionHandler { } +// public void sendMessage(UUID receiverId, String receiverType) { +// Scanner scanner = new Scanner(System.in); +// +// System.out.print("Enter your message: "); +// String content = scanner.nextLine(); +// +// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); +// String messageType = scanner.nextLine(); +// Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); +// while (!allowedTypes.contains(messageType.toUpperCase())) { +// System.out.println("❌ Invalid message type. Try again (TEXT / IMAGE / VIDEO / FILE): "); +// messageType = scanner.nextLine(); +// } +// messageType = messageType.toUpperCase(); +// +// JSONArray attachmentsArray = new JSONArray(); +// +// System.out.print("Do you want to attach files? (yes/no): "); +// if (scanner.nextLine().equalsIgnoreCase("yes")) { +// while (true) { +// System.out.print("File URL: "); +// String fileUrl = scanner.nextLine(); +// +// System.out.print("File Type (IMAGE / VIDEO / FILE): "); +// String fileType = scanner.nextLine(); +// +// JSONObject fileJson = new JSONObject(); +// fileJson.put("file_url", fileUrl); +// fileJson.put("file_type", fileType); +// attachmentsArray.put(fileJson); +// +// System.out.print("Add another file? (yes/no): "); +// if (!scanner.nextLine().equalsIgnoreCase("yes")) { +// break; +// } +// } +// } +// +// +// +// JSONObject messageJson = new JSONObject(); +// messageJson.put("action", "send_message"); +// messageJson.put("receiver_type", receiverType); +// messageJson.put("content", content); +// messageJson.put("message_type", messageType); +// if (receiverType.equals("private")) { +// messageJson.put("receiver_user_id", receiverId.toString()); +// } else { +// messageJson.put("receiver_id", receiverId.toString()); +// } +// +// if (!attachmentsArray.isEmpty()) { +// messageJson.put("attachments", attachmentsArray); +// } +// +// JSONObject response = sendWithResponse(messageJson); +// if (response != null && response.getString("status").equals("success")) { +// System.out.println("✅ Message sent successfully! ID: " + response.getJSONObject("data").getString("message_id")); +// } else { +// System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "no response")); +// } +// +// } + + + public void sendMessage(UUID receiverId, String receiverType) { Scanner scanner = new Scanner(System.in); @@ -2895,40 +3149,53 @@ public class ActionHandler { String content = scanner.nextLine(); System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); - String messageType = scanner.nextLine(); + String messageType = scanner.nextLine().toUpperCase(); Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); - while (!allowedTypes.contains(messageType.toUpperCase())) { - System.out.println("❌ Invalid message type. Try again (TEXT / IMAGE / VIDEO / FILE): "); - messageType = scanner.nextLine(); + while (!allowedTypes.contains(messageType)) { + System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE): "); + messageType = scanner.nextLine().toUpperCase(); } JSONArray attachmentsArray = new JSONArray(); - System.out.print("Do you want to attach files? (yes/no): "); if (scanner.nextLine().equalsIgnoreCase("yes")) { while (true) { System.out.print("File URL: "); String fileUrl = scanner.nextLine(); - System.out.print("File Type (IMAGE / VIDEO / FILE): "); String fileType = scanner.nextLine(); JSONObject fileJson = new JSONObject(); fileJson.put("file_url", fileUrl); - fileJson.put("file_type", fileType); + fileJson.put("file_type", fileType.toUpperCase()); attachmentsArray.put(fileJson); System.out.print("Add another file? (yes/no): "); - if (!scanner.nextLine().equalsIgnoreCase("yes")) { - break; - } + if (!scanner.nextLine().equalsIgnoreCase("yes")) break; } } + // ------------------------ GET receiver_id if private ------------------------ + UUID actualReceiverId = receiverId; + if (receiverType.equals("private")) { + JSONObject req = new JSONObject(); + req.put("action", "get_or_create_private_chat"); + req.put("user1", Session.currentPrivateChatUserId.toString()); + req.put("user2", receiverId.toString()); + + JSONObject chatResponse = sendWithResponse(req); + if (chatResponse == null || !chatResponse.getString("status").equals("success")) { + System.out.println("❌ Failed to fetch private chat ID."); + return; + } + actualReceiverId = UUID.fromString(chatResponse.getJSONObject("data").getString("chat_id")); + } + + // ------------------------ SEND MESSAGE ------------------------ JSONObject messageJson = new JSONObject(); messageJson.put("action", "send_message"); - messageJson.put("receiver_id", receiverId.toString()); messageJson.put("receiver_type", receiverType); + messageJson.put("receiver_id", actualReceiverId.toString()); messageJson.put("content", content); messageJson.put("message_type", messageType); @@ -2938,11 +3205,10 @@ public class ActionHandler { JSONObject response = sendWithResponse(messageJson); if (response != null && response.getString("status").equals("success")) { - System.out.println("✅ Message sent successfully! ID: " + response.getJSONObject("data").getString("message_id")); + System.out.println("✅ Message sent! ID: " + response.getJSONObject("data").getString("message_id")); } else { - System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "no response")); + System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "No response")); } - } diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index 11b11d9..6e5f658 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -3,6 +3,7 @@ package org.to.telegramfinalproject.Client; import org.json.JSONArray; import org.json.JSONObject; import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.Models.ContactEntry; import java.time.LocalDateTime; import java.util.ArrayList; @@ -16,7 +17,7 @@ public class Session { public static List chatList = new ArrayList<>(); public static List archivedChats = new ArrayList<>(); public static List activeChats = new ArrayList<>(); - + public static UUID currentPrivateChatUserId = null; public static volatile boolean forceRefreshChatList = false; public static volatile boolean backToChatList = false; public static boolean inChatListMenu = false; @@ -25,7 +26,7 @@ public class Session { public static volatile boolean refreshCurrentChatMenu = false; public static String currentChatId = null; public static ChatEntry currentChatEntry = null; - + public static List contactEntries = new ArrayList<>(); diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 54d307a..1a97247 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -401,4 +401,42 @@ public class MessageDatabase { } + public List getMessagesForPrivateChat(UUID user1, UUID user2) { + UUID chatId = PrivateChatDatabase.findChatIdByUsers(user1, user2); + if (chatId == null) return new ArrayList<>(); + return findByReceiver("private", chatId); + } + + public static List findByReceiver(String receiverType, UUID receiverId) { + List messages = new ArrayList<>(); + String sql = "SELECT * FROM messages WHERE receiver_type = ? AND receiver_id = ? ORDER BY send_at"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, receiverType); + ps.setObject(2, receiverId); + + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + UUID messageId = (UUID) rs.getObject("message_id"); + UUID senderId = (UUID) rs.getObject("sender_id"); + UUID recId = (UUID) rs.getObject("receiver_id"); + String type = rs.getString("receiver_type"); + String content = rs.getString("content"); + String messageType = rs.getString("message_type"); + LocalDateTime sendAt = rs.getTimestamp("send_at").toLocalDateTime(); + + Message message = new Message(messageId, senderId, recId, type, content, messageType, sendAt); + messages.add(message); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return messages; + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index e13a2e5..d96258f 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -1,9 +1,12 @@ package org.to.telegramfinalproject.Database; +import org.to.telegramfinalproject.Models.PrivateChat; + import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -13,15 +16,15 @@ public class PrivateChatDatabase { public static List getMembers(UUID privateChatId) { - String sql = "SELECT user1, user2 FROM private_chats WHERE chat_id = ?"; + String sql = "SELECT user1_id, user2_id FROM private_chat WHERE chat_id = ?"; try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setObject(1, privateChatId); ResultSet rs = ps.executeQuery(); if (rs.next()) { - UUID user1 = (UUID) rs.getObject("user1"); - UUID user2 = (UUID) rs.getObject("user2"); - return Arrays.asList(user1, user2); + UUID user1 = (UUID) rs.getObject("user1_id"); + UUID user2 = (UUID) rs.getObject("user2_id"); + return new ArrayList<>(Arrays.asList(user1, user2)); } } catch (SQLException e) { e.printStackTrace(); @@ -29,4 +32,105 @@ public class PrivateChatDatabase { return new ArrayList<>(); } + + public static UUID findChatIdByUsers(UUID user1, UUID user2) { + UUID u1 = user1.compareTo(user2) < 0 ? user1 : user2; + UUID u2 = user1.compareTo(user2) < 0 ? user2 : user1; + + String sql = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, u1); + ps.setObject(2, u2); + + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return (UUID) rs.getObject("chat_id"); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + public static UUID getOrCreateChat(UUID user1, UUID user2) { + UUID u1 = user1.compareTo(user2) < 0 ? user1 : user2; + UUID u2 = user1.compareTo(user2) < 0 ? user2 : user1; + + String select = "SELECT chat_id FROM private_chat WHERE user1_id = ? AND user2_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(select)) { + ps.setObject(1, u1); + ps.setObject(2, u2); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return UUID.fromString(rs.getString("chat_id")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + UUID newChatId = UUID.randomUUID(); + String insert = "INSERT INTO private_chat(chat_id, user1_id, user2_id) VALUES (?, ?, ?)"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(insert)) { + ps.setObject(1, newChatId); + ps.setObject(2, u1); + ps.setObject(3, u2); + ps.executeUpdate(); + return newChatId; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + + public static List findChatsOfUser(UUID userId) { + List chats = new ArrayList<>(); + String sql = "SELECT * FROM private_chat WHERE user1_id = ? OR user2_id = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setObject(1, userId); + ps.setObject(2, userId); + + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + UUID chatId = (UUID) rs.getObject("chat_id"); + UUID user1 = (UUID) rs.getObject("user1_id"); + UUID user2 = (UUID) rs.getObject("user2_id"); + + chats.add(new PrivateChat(chatId, user1, user2)); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return chats; + } + + + public static UUID getOtherUserInChat(UUID chatId, UUID currentUserId) { + String sql = "SELECT user1_id, user2_id FROM private_chat WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + UUID user1 = (UUID) rs.getObject("user1_id"); + UUID user2 = (UUID) rs.getObject("user2_id"); + return currentUserId.equals(user1) ? user2 : user1; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java index aa98d0b..8176576 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -13,6 +13,7 @@ public class ChatEntry { private String type; private LocalDateTime lastMessageTime; private boolean archived = false; + private UUID otherUser; private boolean isOwner = false; @@ -133,4 +134,10 @@ public class ChatEntry { } } + public void setOtherUserId(UUID otherId) {this.otherUser = otherId; + } + + public UUID getOtherUserId(){ + return otherUser; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java new file mode 100644 index 0000000..c0e4341 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java @@ -0,0 +1,44 @@ +package org.to.telegramfinalproject.Models; + +import java.util.UUID; + +public class ContactEntry { + private UUID contactId; // internal UUID + private String userId; // public ID + private String profileName; + private String imageUrl; + private boolean isBlocked; + + public ContactEntry(UUID contactId, String userId, String profileName, String imageUrl, boolean isBlocked) { + this.contactId = contactId; + this.userId = userId; + this.profileName = profileName; + this.imageUrl = imageUrl; + this.isBlocked = isBlocked; + } + + public UUID getContactId() { + return contactId; + } + + public String getUserId() { + return userId; + } + + public String getProfileName() { + return profileName; + } + + public String getImageUrl() { + return imageUrl; + } + + public boolean isBlocked() { + return isBlocked; + } + + @Override + public String toString() { + return profileName + " (" + userId + ")" + (isBlocked ? " [Blocked]" : ""); + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java b/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java index 372600b..20caa6d 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java +++ b/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java @@ -9,7 +9,7 @@ public class PrivateChat { private UUID user2_id; private LocalDateTime created_at; - public PrivateChat(UUID chat_id, UUID user1_id, UUID user2_id, LocalDateTime created_at){ + public PrivateChat(UUID chat_id, UUID user1_id, UUID user2_id){ this.chat_id = chat_id; this.user1_id =user1_id; this.user2_id =user2_id; diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 9596845..3b82293 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -103,38 +103,84 @@ public class ClientHandler implements Runnable { List activeChatList = new ArrayList<>(); + JSONArray contactList = new JSONArray(); for (Contact contact : contacts) { User target = userDatabase.findByInternalUUID(contact.getContact_id()); if (target == null) continue; - LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(user.getInternal_uuid(), target.getInternal_uuid(), "private"); -// chatList.add(new ChatEntry( -// target.getInternal_uuid(), // internal UUID -// target.getUser_id(), // public display ID + JSONObject c = new JSONObject(); + c.put("user_id", contact.getUser_id().toString()); + c.put("contact_id", contact.getContact_id().toString()); + c.put("is_blocked", contact.getIs_blocked()); + + c.put("profile_name", target.getProfile_name()); + c.put("image_url", target.getImage_url()); + + contactList.put(c); + } + + + +// for (Contact contact : contacts) { +// User target = userDatabase.findByInternalUUID(contact.getContact_id()); +// if (target == null) continue; +// LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(user.getInternal_uuid(), target.getInternal_uuid(), "private"); +// +//// chatList.add(new ChatEntry( +//// target.getInternal_uuid(), // internal UUID +//// target.getUser_id(), // public display ID +//// target.getProfile_name(), +//// target.getImage_url(), +//// "private", +//// last, +//// false, +//// false +//// )); +// +// UUID targetId = target.getInternal_uuid(); +// +// ChatEntry entry = new ChatEntry( +// targetId, +// target.getUser_id(), // target.getProfile_name(), // target.getImage_url(), // "private", // last, // false, // false -// )); +// ); +// +// +// +// } - UUID targetId = target.getInternal_uuid(); + + + List privateChats = PrivateChatDatabase.findChatsOfUser(currentUser.getInternal_uuid()); + for (PrivateChat chat : privateChats) { + UUID otherId = chat.getUser1_id().equals(currentUser.getInternal_uuid()) ? + chat.getUser2_id() : chat.getUser1_id(); + + User otherUser = userDatabase.findByInternalUUID(otherId); + if (otherUser == null) continue; + + LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTimeBetween( + currentUser.getInternal_uuid(), otherId, "private" + ); ChatEntry entry = new ChatEntry( - targetId, - target.getUser_id(), - target.getProfile_name(), - target.getImage_url(), + chat.getChat_id(), // 🔹 real private chat_id + otherUser.getUser_id(), // نمایش عمومی طرف مقابل + otherUser.getProfile_name(), + otherUser.getImage_url(), "private", - last, + lastMessageTime, false, false ); + entry.setOtherUserId(otherId); - - - if (archivedChatIds.contains(targetId)) { + if (archivedChatIds.contains(chat.getChat_id())) { archivedChatList.add(entry); chatList.add(entry); } else { @@ -144,6 +190,7 @@ public class ClientHandler implements Runnable { } + for (Group group : groups) { LocalDateTime last = MessageDatabase.getLastMessageTime(group.getInternal_uuid(), "group"); boolean isOwner = GroupDatabase.isOwner(group.getInternal_uuid(), user.getInternal_uuid()); @@ -240,7 +287,8 @@ public class ClientHandler implements Runnable { JSONObject userData = JsonUtil.userToJson(user); userData.put("chat_list", JsonUtil.chatListToJson(chatList)); userData.put("archived_chat_list", JsonUtil.chatListToJson(archivedChatList)); - userData.put("active_chat_lis", JsonUtil.chatListToJson(activeChatList)); + userData.put("active_chat_list", JsonUtil.chatListToJson(activeChatList)); + userData.put("contact_list", contactList); response = new ResponseModel("success", "Welcome " + user.getProfile_name(), userData); } break; @@ -605,33 +653,86 @@ public class ClientHandler implements Runnable { List contacts = ContactDatabase.getContacts(currentUser.getInternal_uuid()); List groups = GroupDatabase.getGroupsByUser(currentUser.getInternal_uuid()); List channels = ChannelDatabase.getChannelsByUser(currentUser.getInternal_uuid()); - + List archivedChatIds = ArchivedChatDatabase.getArchivedChats(currentUser.getInternal_uuid()); List chatList = new ArrayList<>(); + List archivedChatList = new ArrayList<>(); + List activeChatList = new ArrayList<>(); - for (Contact contact : contacts) { - User target = userDatabase.findByInternalUUID(contact.getContact_id()); - if (target == null) continue; - LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(currentUser.getInternal_uuid(), target.getInternal_uuid(), "private"); - chatList.add(new ChatEntry( - target.getInternal_uuid(), - target.getUser_id(), - target.getProfile_name(), - target.getImage_url(), + + + +// for (Contact contact : contacts) { +// User target = userDatabase.findByInternalUUID(contact.getContact_id()); +// if (target == null) continue; +// +// LocalDateTime last = MessageDatabase.getLastMessageTimeBetween(currentUser.getInternal_uuid(), target.getInternal_uuid(), "private"); +// +// chatList.add(new ChatEntry( +// target.getInternal_uuid(), +// target.getUser_id(), +// target.getProfile_name(), +// target.getImage_url(), +// "private", +// last, +// false, +// false +// )); +// } +// + + List privateChats = PrivateChatDatabase.findChatsOfUser(currentUser.getInternal_uuid()); + for (PrivateChat chat : privateChats) { + UUID otherId = chat.getUser1_id().equals(currentUser.getInternal_uuid()) ? + chat.getUser2_id() : chat.getUser1_id(); + + User otherUser = userDatabase.findByInternalUUID(otherId); + if (otherUser == null) continue; + + LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTimeBetween( + currentUser.getInternal_uuid(), otherId, "private" + ); + + ChatEntry entry = new ChatEntry( + chat.getChat_id(), // 🔹 real private chat_id + otherUser.getUser_id(), // نمایش عمومی طرف مقابل + otherUser.getProfile_name(), + otherUser.getImage_url(), "private", - last, + lastMessageTime, false, false - )); + ); + entry.setOtherUserId(otherId); + + if (archivedChatIds.contains(chat.getChat_id())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + for (Group group : groups) { LocalDateTime last = MessageDatabase.getLastMessageTime(group.getInternal_uuid(), "group"); boolean isOwner = GroupDatabase.isOwner(group.getInternal_uuid(), currentUser.getInternal_uuid()); boolean isAdmin = GroupDatabase.isAdmin(group.getInternal_uuid(), currentUser.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// group.getInternal_uuid(), +// group.getGroup_id(), +// group.getGroup_name(), +// group.getImage_url(), +// "group", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( group.getInternal_uuid(), group.getGroup_id(), group.getGroup_name(), @@ -640,7 +741,17 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(group.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } + + } for (Channel channel : channels) { @@ -648,7 +759,18 @@ public class ClientHandler implements Runnable { boolean isOwner = ChannelDatabase.isOwner(channel.getInternal_uuid(), currentUser.getInternal_uuid()); boolean isAdmin = ChannelDatabase.isAdmin(channel.getInternal_uuid(), currentUser.getInternal_uuid()); - chatList.add(new ChatEntry( +// chatList.add(new ChatEntry( +// channel.getInternal_uuid(), +// channel.getChannel_id(), +// channel.getChannel_name(), +// channel.getImage_url(), +// "channel", +// last, +// isOwner, +// isAdmin +// )); + + ChatEntry entry = new ChatEntry( channel.getInternal_uuid(), channel.getChannel_id(), channel.getChannel_name(), @@ -657,17 +779,39 @@ public class ClientHandler implements Runnable { last, isOwner, isAdmin - )); + ); + + if (archivedChatIds.contains(channel.getInternal_uuid())) { + archivedChatList.add(entry); + chatList.add(entry); + } else { + activeChatList.add(entry); + chatList.add(entry); + } } + activeChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + + archivedChatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + + chatList.sort((a, b) -> { if (a.getLastMessageTime() == null) return 1; if (b.getLastMessageTime() == null) return -1; return b.getLastMessageTime().compareTo(a.getLastMessageTime()); }); - JSONObject data = new JSONObject(); data.put("chat_list", JsonUtil.chatListToJson(chatList)); + data.put("archived_chat_list", JsonUtil.chatListToJson(archivedChatList)); + data.put("active_chat_list", JsonUtil.chatListToJson(activeChatList)); response = new ResponseModel("success", "Chat list updated.", data); break; @@ -1116,13 +1260,16 @@ public class ClientHandler implements Runnable { switch (receiverType) { case "private" -> { - User otherUser = new userDatabase().findByInternalUUID(UUID.fromString(receiverId)); - if (otherUser == null) { - response = new ResponseModel("error", "User not found."); + List members = PrivateChatDatabase.getMembers(UUID.fromString(receiverId)); + if (members.size() != 2) { + response = new ResponseModel("error", "Invalid private chat."); break; } - messages = MessageDatabase.privateChatHistory(currentUser.getInternal_uuid(), otherUser.getInternal_uuid()); + + UUID otherUserId = members.get(0).equals(currentUser.getInternal_uuid()) ? members.get(1) : members.get(0); + messages = MessageDatabase.privateChatHistory(currentUser.getInternal_uuid(), otherUserId); } + case "group" -> { Group group = GroupDatabase.findByInternalUUID(UUID.fromString(receiverId)); if (group == null) { @@ -1836,6 +1983,45 @@ public class ClientHandler implements Runnable { break; + case "get_or_create_private_chat": { + if (currentUser == null) { + response = new ResponseModel("error", "Unauthorized. Please login first."); + break; + } + + try { + UUID user1 = UUID.fromString(requestJson.getString("user1")); + UUID user2 = UUID.fromString(requestJson.getString("user2")); + + UUID chatId = PrivateChatDatabase.getOrCreateChat(user1, user2); + + JSONObject data = new JSONObject(); + data.put("chat_id", chatId.toString()); + response = new ResponseModel("success", "Private chat ID retrieved.", data); + } catch (Exception e) { + e.printStackTrace(); + response = new ResponseModel("error", "Invalid data or internal error."); + } + + break; + } + + + + case "get_private_chat_target": { + UUID chatId = UUID.fromString(requestJson.getString("chat_id")); + userId = currentUser.getInternal_uuid(); + + UUID targetId = PrivateChatDatabase.getOtherUserInChat(chatId, userId); + if (targetId == null) { + response = new ResponseModel("error", "Could not find other user."); + } else { + JSONObject data = new JSONObject(); + data.put("target_id", targetId.toString()); + response = new ResponseModel("success", "Target fetched.", data); + } + break; + } @@ -1904,8 +2090,15 @@ public class ClientHandler implements Runnable { UUID messageId = UUID.randomUUID(); UUID senderId = currentUser.getInternal_uuid(); - UUID receiverId = UUID.fromString(json.getString("receiver_id")); String receiverType = json.getString("receiver_type"); + UUID receiverId; + + if (receiverType.equals("private")) { + UUID receiverUserId = UUID.fromString(json.getString("receiver_user_id")); + receiverId = PrivateChatDatabase.getOrCreateChat(senderId, receiverUserId); + } else { + receiverId = UUID.fromString(json.getString("receiver_id")); // group یا channel + } String content = json.optString("content", ""); String messageType = json.optString("message_type", "TEXT"); From c6616863a1cb143e5be2e291a4d88673026f1cf3 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 1 Aug 2025 15:58:35 +0330 Subject: [PATCH 07/11] block/unblock fixed --- .../Client/ActionHandler.java | 46 +++++++++--------- .../Database/MessageDatabase.java | 14 ++---- .../Server/ClientHandler.java | 47 +++++++++---------- 3 files changed, 49 insertions(+), 58 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index ddfd379..a23588c 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1140,6 +1140,21 @@ public class ActionHandler { return false; } + + JSONObject reqTarget = new JSONObject(); + reqTarget.put("action", "get_private_chat_target"); + reqTarget.put("chat_id", chat.getId()); + + JSONObject resTarget = sendWithResponse(reqTarget); + if (resTarget == null || !resTarget.getString("status").equals("success")) { + System.out.println("❌ Failed to fetch target user for private chat."); + return false; + } + + String otherUserId = resTarget.getJSONObject("data").getString("target_id"); + chat.setOtherUserId(UUID.fromString(otherUserId)); + + JSONObject req = new JSONObject(); req.put("action", "view_profile"); req.put("target_id", chat.getOtherUserId()); // internal UUID @@ -1173,7 +1188,7 @@ public class ActionHandler { String input = scanner.nextLine(); switch (input) { case "1" -> sendMessage(chat.getId(), "private"); - case "2" -> toggleBlock(chat.getId()); + case "2" -> toggleBlock(chat.getOtherUserId()); case "3" -> { deleteChat(chat.getId(), false); return false; @@ -2013,6 +2028,8 @@ public class ActionHandler { private void toggleBlock(UUID userId) { + + JSONObject req = new JSONObject(); req.put("action", "toggle_block"); req.put("user_id", Session.getUserUUID()); @@ -3142,7 +3159,7 @@ public class ActionHandler { - public void sendMessage(UUID receiverId, String receiverType) { + public void sendMessage(UUID chatId, String receiverType) { Scanner scanner = new Scanner(System.in); System.out.print("Enter your message: "); @@ -3163,11 +3180,11 @@ public class ActionHandler { System.out.print("File URL: "); String fileUrl = scanner.nextLine(); System.out.print("File Type (IMAGE / VIDEO / FILE): "); - String fileType = scanner.nextLine(); + String fileType = scanner.nextLine().toUpperCase(); JSONObject fileJson = new JSONObject(); fileJson.put("file_url", fileUrl); - fileJson.put("file_type", fileType.toUpperCase()); + fileJson.put("file_type", fileType); attachmentsArray.put(fileJson); System.out.print("Add another file? (yes/no): "); @@ -3175,27 +3192,11 @@ public class ActionHandler { } } - // ------------------------ GET receiver_id if private ------------------------ - UUID actualReceiverId = receiverId; - if (receiverType.equals("private")) { - JSONObject req = new JSONObject(); - req.put("action", "get_or_create_private_chat"); - req.put("user1", Session.currentPrivateChatUserId.toString()); - req.put("user2", receiverId.toString()); - - JSONObject chatResponse = sendWithResponse(req); - if (chatResponse == null || !chatResponse.getString("status").equals("success")) { - System.out.println("❌ Failed to fetch private chat ID."); - return; - } - actualReceiverId = UUID.fromString(chatResponse.getJSONObject("data").getString("chat_id")); - } - - // ------------------------ SEND MESSAGE ------------------------ + // 🔹 فقط ارسال پیام با chat_id و receiver_type JSONObject messageJson = new JSONObject(); messageJson.put("action", "send_message"); messageJson.put("receiver_type", receiverType); - messageJson.put("receiver_id", actualReceiverId.toString()); + messageJson.put("receiver_id", chatId.toString()); messageJson.put("content", content); messageJson.put("message_type", messageType); @@ -3216,6 +3217,7 @@ public class ActionHandler { + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 1a97247..4b4e4b4 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -258,25 +258,18 @@ public class MessageDatabase { return result; } - public static List privateChatHistory(UUID user1, UUID user2) { + public static List privateChatHistory(UUID chatId) { List result = new ArrayList<>(); String sql = """ SELECT * FROM messages WHERE receiver_type = 'private' - AND ( - (sender_id = ? AND receiver_id = ?) - OR (sender_id = ? AND receiver_id = ?) - ) + AND receiver_id = ? ORDER BY send_at """; try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setObject(1, user1); - stmt.setObject(2, user2); - stmt.setObject(3, user2); - stmt.setObject(4, user1); - + stmt.setObject(1, chatId); ResultSet rs = stmt.executeQuery(); while (rs.next()) { result.add(extractMessage(rs)); @@ -289,6 +282,7 @@ public class MessageDatabase { + public static List groupChatHistory(UUID groupId) { List result = new ArrayList<>(); String sql = """ diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 3b82293..5b611e2 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -164,13 +164,12 @@ public class ClientHandler implements Runnable { User otherUser = userDatabase.findByInternalUUID(otherId); if (otherUser == null) continue; - LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTimeBetween( - currentUser.getInternal_uuid(), otherId, "private" - ); + LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTime(chat.getChat_id(), "private"); + ChatEntry entry = new ChatEntry( - chat.getChat_id(), // 🔹 real private chat_id - otherUser.getUser_id(), // نمایش عمومی طرف مقابل + chat.getChat_id(), + otherUser.getUser_id(), otherUser.getProfile_name(), otherUser.getImage_url(), "private", @@ -690,13 +689,12 @@ public class ClientHandler implements Runnable { User otherUser = userDatabase.findByInternalUUID(otherId); if (otherUser == null) continue; - LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTimeBetween( - currentUser.getInternal_uuid(), otherId, "private" - ); + LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTime(chat.getChat_id(), "private"); + ChatEntry entry = new ChatEntry( - chat.getChat_id(), // 🔹 real private chat_id - otherUser.getUser_id(), // نمایش عمومی طرف مقابل + chat.getChat_id(), + otherUser.getUser_id(), otherUser.getProfile_name(), otherUser.getImage_url(), "private", @@ -1260,16 +1258,17 @@ public class ClientHandler implements Runnable { switch (receiverType) { case "private" -> { - List members = PrivateChatDatabase.getMembers(UUID.fromString(receiverId)); - if (members.size() != 2) { - response = new ResponseModel("error", "Invalid private chat."); + UUID chatId = UUID.fromString(receiverId); + List members = PrivateChatDatabase.getMembers(chatId); + if (!members.contains(currentUser.getInternal_uuid())) { + response = new ResponseModel("error", "You're not a member of this private chat."); break; } - UUID otherUserId = members.get(0).equals(currentUser.getInternal_uuid()) ? members.get(1) : members.get(0); - messages = MessageDatabase.privateChatHistory(currentUser.getInternal_uuid(), otherUserId); + messages = MessageDatabase.privateChatHistory(chatId); } + case "group" -> { Group group = GroupDatabase.findByInternalUUID(UUID.fromString(receiverId)); if (group == null) { @@ -2083,6 +2082,8 @@ public class ClientHandler implements Runnable { } + + private ResponseModel handleSendMessage(JSONObject json) { try { if (currentUser == null) @@ -2093,12 +2094,8 @@ public class ClientHandler implements Runnable { String receiverType = json.getString("receiver_type"); UUID receiverId; - if (receiverType.equals("private")) { - UUID receiverUserId = UUID.fromString(json.getString("receiver_user_id")); - receiverId = PrivateChatDatabase.getOrCreateChat(senderId, receiverUserId); - } else { - receiverId = UUID.fromString(json.getString("receiver_id")); // group یا channel - } + receiverId = UUID.fromString(json.getString("receiver_id")); + String content = json.optString("content", ""); String messageType = json.optString("message_type", "TEXT"); @@ -2123,15 +2120,13 @@ public class ClientHandler implements Runnable { return new ResponseModel("error", "Message inserted but failed to attach files."); } - - + // Send real-time message Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now()); List receivers = getReceiversForChat(receiverId, receiverType); receivers.remove(senderId); - RealTimeEventDispatcher.sendNewMessage(msg, receivers); - + // Update chat list (last_message_time) JSONObject chatUpdate = new JSONObject(); chatUpdate.put("chat_id", receiverId.toString()); chatUpdate.put("chat_type", receiverType); @@ -2145,7 +2140,6 @@ public class ClientHandler implements Runnable { RealTimeEventDispatcher.sendToUser(receiver, chatPayload); } - JSONObject data = new JSONObject(); data.put("message_id", messageId.toString()); return new ResponseModel("success", "Message sent successfully.", data); @@ -2156,6 +2150,7 @@ public class ClientHandler implements Runnable { } } + private List getReceiversForChat(UUID receiverId, String receiverType) { switch (receiverType) { case "private": From 6b320b63599b1a54e61a1918a2c7b72d31c6d73f Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 1 Aug 2025 16:08:12 +0330 Subject: [PATCH 08/11] Update init.sql --- src/main/resources/init.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index 981936b..5e5c7a4 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -42,9 +42,13 @@ CREATE TABLE IF NOT EXISTS private_chat ( chat_id UUID PRIMARY KEY, user1_id UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, user2_id UUID REFERENCES users(internal_uuid) ON DELETE SET NULL, + user1_deleted BOOLEAN DEFAULT FALSE, + user2_deleted BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_users UNIQUE (user1_id, user2_id) ); + + CREATE TABLE IF NOT EXISTS groups ( internal_uuid UUID PRIMARY KEY, group_id VARCHAR(70) UNIQUE, From 3cdbb7e24b592b1195db56f9dce7d23cc387b6b2 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 1 Aug 2025 20:59:59 +0330 Subject: [PATCH 09/11] Work on delete chat --- .../Client/ActionHandler.java | 3 +- .../Database/MessageDatabase.java | 48 +++++++ .../Database/PrivateChatDatabase.java | 130 +++++++++++++++++- .../Models/PrivateChat.java | 12 ++ .../Server/ClientHandler.java | 61 +++++++- 5 files changed, 242 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index a23588c..55f2bfc 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1237,6 +1237,7 @@ public class ActionHandler { return true; } + private JSONObject getGroupPermissions(UUID groupId) { JSONObject req = new JSONObject(); req.put("action", "get_group_permissions"); @@ -2013,7 +2014,7 @@ public class ActionHandler { private void deleteChat(UUID targetId, boolean both) { JSONObject req = new JSONObject(); req.put("action", "delete_private_chat"); - req.put("target_id", targetId.toString()); + req.put("chat_id", targetId.toString()); req.put("both", both); send(req); diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 4b4e4b4..eea9f79 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -65,6 +65,54 @@ public class MessageDatabase { } } + public static void markGloballyDeleted(UUID chatId) { + String sql = "UPDATE messages SET deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static void logDeletedMessagesFor(UUID chatId, UUID userId) { + List messages = MessageDatabase.privateChatHistory(chatId); + for (Message message : messages) { + if (!isMessageDeleted(message.getMessage_id(), userId)) { + String sql = """ + INSERT INTO deleted_messages (message_id, user_id) + VALUES (?, ?) + ON CONFLICT (message_id, user_id) DO NOTHING + """; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, message.getMessage_id()); + ps.setObject(2, userId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + } + + public static boolean isMessageDeleted(UUID messageId, UUID userId) { + String sql = "SELECT 1 FROM deleted_messages WHERE message_id = ? AND user_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, userId); + ResultSet rs = ps.executeQuery(); + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + + + public void markMessageAsRead(UUID messageId, UUID userId) { String sql = "INSERT INTO message_receipts (message_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; try (Connection conn = ConnectionDb.connect(); diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index d96258f..7487b1f 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -2,10 +2,7 @@ package org.to.telegramfinalproject.Database; import org.to.telegramfinalproject.Models.PrivateChat; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; +import java.sql.*; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -133,4 +130,129 @@ public class PrivateChatDatabase { return null; } + + private static PrivateChat extractPrivateChat(ResultSet rs) throws SQLException { + UUID chatId = UUID.fromString(rs.getString("chat_id")); + UUID user1 = rs.getObject("user1_id", UUID.class); + UUID user2 = rs.getObject("user2_id", UUID.class); + boolean user1Deleted = rs.getBoolean("user1_deleted"); + boolean user2Deleted = rs.getBoolean("user2_deleted"); + LocalDateTime createdAt = rs.getTimestamp("created_at").toLocalDateTime(); + + return new PrivateChat(chatId, user1, user2, user1Deleted, user2Deleted, createdAt); + } + + public static void markChatDeleted(UUID userId, UUID chatId) { + String sql = """ + UPDATE private_chat + SET user1_deleted = CASE WHEN user1_id = ? THEN true ELSE user1_deleted END, + user2_deleted = CASE WHEN user2_id = ? THEN true ELSE user2_deleted END + WHERE chat_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + stmt.setObject(2, userId); + stmt.setObject(3, chatId); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static void unmarkChatDeleted(UUID userId, UUID chatId) { + String sql = """ + UPDATE private_chat + SET user1_deleted = CASE WHEN user1_id = ? THEN false ELSE user1_deleted END, + user2_deleted = CASE WHEN user2_id = ? THEN false ELSE user2_deleted END + WHERE chat_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + stmt.setObject(2, userId); + stmt.setObject(3, chatId); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static void markUser1Deleted(UUID chatId) { + updateBoolField(chatId, "user1_deleted", true); + } + + public static void markUser2Deleted(UUID chatId) { + updateBoolField(chatId, "user2_deleted", true); + } + + public static void markBothDeleted(UUID chatId) { + String sql = "UPDATE private_chat SET user1_deleted = true, user2_deleted = true WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private static void updateBoolField(UUID chatId, String field, boolean value) { + String sql = "UPDATE private_chat SET " + field + " = ? WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setBoolean(1, value); + ps.setObject(2, chatId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public static PrivateChat findById(UUID chatId) { + String sql = "SELECT * FROM private_chat WHERE chat_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, chatId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + UUID user1_id = (UUID) rs.getObject("user1_id"); + UUID user2_id = (UUID) rs.getObject("user2_id"); + boolean user1_deleted = rs.getBoolean("user1_deleted"); + boolean user2_deleted = rs.getBoolean("user2_deleted"); + LocalDateTime created_at = rs.getTimestamp("created_at").toLocalDateTime(); + + return new PrivateChat(chatId, user1_id, user2_id, user1_deleted, user2_deleted, created_at); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + public static void clearDeletedFlag(UUID senderId, UUID receiverId) { + String sql = """ + UPDATE private_chat + SET user1_deleted = CASE WHEN user1_id = ? THEN false ELSE user1_deleted END, + user2_deleted = CASE WHEN user2_id = ? THEN false ELSE user2_deleted END + 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, senderId); + ps.setObject(2, senderId); + ps.setObject(3, senderId); + ps.setObject(4, receiverId); + ps.setObject(5, receiverId); + ps.setObject(6, senderId); + ps.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java b/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java index 20caa6d..17ca2a9 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java +++ b/src/main/java/org/to/telegramfinalproject/Models/PrivateChat.java @@ -5,6 +5,8 @@ import java.util.UUID; public class PrivateChat { private final UUID chat_id; + private boolean user1_deleted; + private boolean user2_deleted; private UUID user1_id; private UUID user2_id; private LocalDateTime created_at; @@ -16,6 +18,16 @@ public class PrivateChat { this.created_at =created_at; } + public PrivateChat(UUID chatId, UUID user1, UUID user2, boolean user1Deleted, boolean user2Deleted, LocalDateTime createdAt) { + this.chat_id = chatId; + this.user1_id = user1; + this.user2_id = user2; + this.user1_deleted = user1Deleted; + this.user2_deleted = user2Deleted; + this.created_at = createdAt; + } + + public void setUser1_id(UUID user1_id){this.user1_id =user1_id;} public void setUser2_id(UUID user2_id){this.user2_id =user2_id;} public void setCreated_at(LocalDateTime created_at){this.created_at = created_at;} diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 5b611e2..49862fb 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1432,26 +1432,33 @@ public class ClientHandler implements Runnable { - case "delete_private_chat" : { + case "delete_private_chat": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); break; } - UUID targetId = UUID.fromString(requestJson.getString("target_id")); + + // دریافت شناسه چت و نوع حذف (یک‌طرفه یا دوطرفه) + UUID targetId = UUID.fromString(requestJson.getString("chat_id")); boolean both = requestJson.getBoolean("both"); - //RealTime + // Real-Time Event Dispatch (اطلاع‌رسانی ریل تایم) if (both) { + // حذف دوطرفه RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); RealTimeEventDispatcher.notifyChatDeleted("private", currentUser.getInternal_uuid(), List.of(targetId)); } else { + // حذف یک‌طرفه RealTimeEventDispatcher.notifyChatDeleted("private", targetId, List.of(currentUser.getInternal_uuid())); } - response = PrivateChatService.deletePrivateChat(currentUser.getInternal_uuid(), targetId, both); + + // فراخوانی متد حذف چت + response = handleDeleteChat(requestJson); break; } + case "get_group_permissions": { if (currentUser == null) { response = new ResponseModel("error", "Unauthorized. Please login first."); @@ -2025,8 +2032,6 @@ public class ClientHandler implements Runnable { - - default: response = new ResponseModel("error", "Unknown action: " + action); } @@ -2085,6 +2090,7 @@ public class ClientHandler implements Runnable { private ResponseModel handleSendMessage(JSONObject json) { + try { if (currentUser == null) return new ResponseModel("error", "Unauthorized. Please login first."); @@ -2093,9 +2099,13 @@ public class ClientHandler implements Runnable { UUID senderId = currentUser.getInternal_uuid(); String receiverType = json.getString("receiver_type"); UUID receiverId; - receiverId = UUID.fromString(json.getString("receiver_id")); + if(Objects.equals(receiverType, "private")){ + PrivateChatDatabase.clearDeletedFlag(senderId, receiverId); + } + + String content = json.optString("content", ""); String messageType = json.optString("message_type", "TEXT"); @@ -2165,5 +2175,42 @@ public class ClientHandler implements Runnable { } + private ResponseModel handleDeleteChat(JSONObject json) { + try { + if (currentUser == null) + return new ResponseModel("error", "Unauthorized"); + + UUID chatId = UUID.fromString(json.getString("chat_id")); + boolean bothSides = json.optBoolean("both_sides", false); + + PrivateChat chat = PrivateChatDatabase.findById(chatId); + if (chat == null) return new ResponseModel("error", "Chat not found."); + + UUID self = currentUser.getInternal_uuid(); + UUID other = chat.getUser1_id().equals(self) ? chat.getUser2_id() : chat.getUser1_id(); + + if (bothSides) { + PrivateChatDatabase.markBothDeleted(chatId); + MessageDatabase.markGloballyDeleted(chatId); + MessageDatabase.logDeletedMessagesFor(chatId, self); + MessageDatabase.logDeletedMessagesFor(chatId, other); + return new ResponseModel("success", "Chat deleted for both sides."); + } else { + if (chat.getUser1_id().equals(self)) + PrivateChatDatabase.markUser1Deleted(chatId); + else + PrivateChatDatabase.markUser2Deleted(chatId); + + MessageDatabase.logDeletedMessagesFor(chatId, self); + return new ResponseModel("success", "Chat deleted (one-sided)."); + } + + } catch (Exception e) { + e.printStackTrace(); + return new ResponseModel("error", "Exception while deleting chat."); + } + } + + } \ No newline at end of file From 364e675bf723a4694e4de39c6e4a81541161aebc Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Sat, 2 Aug 2025 23:58:02 +0330 Subject: [PATCH 10/11] Delete chat (one/both side) --- .../Client/ActionHandler.java | 4 ++-- .../Database/MessageDatabase.java | 2 +- .../Database/PrivateChatDatabase.java | 19 +++++++++---------- .../Server/ClientHandler.java | 3 +-- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 55f2bfc..21303a4 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1191,11 +1191,11 @@ public class ActionHandler { case "2" -> toggleBlock(chat.getOtherUserId()); case "3" -> { deleteChat(chat.getId(), false); - return false; + return true; } case "4" -> { deleteChat(chat.getId(), true); - return false; + return true; } case "5" -> { JSONObject reqProfile = new JSONObject(); diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index eea9f79..fcdaf7c 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -66,7 +66,7 @@ public class MessageDatabase { } public static void markGloballyDeleted(UUID chatId) { - String sql = "UPDATE messages SET deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'"; + String sql = "UPDATE messages SET is_deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'"; try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setObject(1, chatId); ps.executeUpdate(); diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index 7487b1f..97ddbcf 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -231,22 +231,20 @@ public class PrivateChatDatabase { } - public static void clearDeletedFlag(UUID senderId, UUID receiverId) { + public static void clearDeletedFlag(UUID senderId, UUID chatId) { String sql = """ UPDATE private_chat - SET user1_deleted = CASE WHEN user1_id = ? THEN false ELSE user1_deleted END, - user2_deleted = CASE WHEN user2_id = ? THEN false ELSE user2_deleted END - WHERE (user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?) - """; + SET user1_deleted = CASE WHEN user1_id = ? THEN FALSE ELSE user1_deleted END, + user2_deleted = CASE WHEN user2_id = ? THEN FALSE ELSE user2_deleted END + WHERE chat_id = ? + """; try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setObject(1, senderId); ps.setObject(2, senderId); - ps.setObject(3, senderId); - ps.setObject(4, receiverId); - ps.setObject(5, receiverId); - ps.setObject(6, senderId); - ps.executeUpdate(); + ps.setObject(3, chatId); + int rows = ps.executeUpdate(); + System.out.println("✅ clearDeletedFlag updated rows = " + rows); } catch (SQLException e) { e.printStackTrace(); } @@ -255,4 +253,5 @@ public class PrivateChatDatabase { + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 49862fb..fef8a04 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -2181,8 +2181,7 @@ public class ClientHandler implements Runnable { return new ResponseModel("error", "Unauthorized"); UUID chatId = UUID.fromString(json.getString("chat_id")); - boolean bothSides = json.optBoolean("both_sides", false); - + boolean bothSides = json.optBoolean("both_sides", json.optBoolean("both", false)); PrivateChat chat = PrivateChatDatabase.findById(chatId); if (chat == null) return new ResponseModel("error", "Chat not found."); From fd399372a537230115cad796649d240cf8669e0d Mon Sep 17 00:00:00 2001 From: Asal Lotfi Date: Sun, 3 Aug 2025 11:41:41 +0330 Subject: [PATCH 11/11] init.sql updated. --- src/main/resources/init.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index 5e5c7a4..e4b7d1c 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -71,7 +71,7 @@ CREATE TABLE group_members ( CREATE TABLE IF NOT EXISTS channels ( channel_id VARCHAR(70) UNIQUE, - internal_uuid UUID PRIMARY KEY,, + internal_uuid UUID PRIMARY KEY, channel_name VARCHAR(100), creator_id UUID REFERENCES users(internal_uuid), image_url TEXT,