From fb19b5559374d65d82248754a4d0243e88b9a85f Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 12 Jun 2025 02:01:06 +0330 Subject: [PATCH] Work on add contact to contact list --- .../Client/ActionHandler.java | 242 +++++++++-------- .../Client/IncomingMessageListener.java | 86 ++++++ .../Client/TelegramClient.java | 9 +- .../Database/ChannelDatabase.java | 111 ++++++++ .../Database/ContactDatabase.java | 21 +- .../Database/GroupDatabase.java | 106 ++++++++ .../Database/MessageDatabase.java | 35 +++ .../Database/userDatabase.java | 1 - .../Models/ContactRequestModel.java | 23 ++ .../Models/SearchResultModel.java | 59 +++++ .../Server/ClientHandler.java | 72 +++-- .../Server/ContactService.java | 15 ++ .../Server/MainServer.java | 2 +- .../Server/RealTimeEventDispatcher.java | 248 ++++++++++++++++++ .../to/telegramfinalproject/Contact_Cell.fxml | 27 ++ .../org/to/telegramfinalproject/MainPage.fxml | 52 ++++ .../telegramfinalproject/Searching_view.fxml | 68 +++++ 17 files changed, 1035 insertions(+), 142 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java create mode 100644 src/main/java/org/to/telegramfinalproject/Models/ContactRequestModel.java create mode 100644 src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java create mode 100644 src/main/java/org/to/telegramfinalproject/Server/ContactService.java create mode 100644 src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java create mode 100644 src/main/resources/org/to/telegramfinalproject/Contact_Cell.fxml create mode 100644 src/main/resources/org/to/telegramfinalproject/MainPage.fxml create mode 100644 src/main/resources/org/to/telegramfinalproject/Searching_view.fxml diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 2555c2c..4ba057c 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1,17 +1,17 @@ package org.to.telegramfinalproject.Client; +import org.json.JSONArray; +import org.json.JSONObject; +import org.to.telegramfinalproject.Models.ChatEntry; +import org.to.telegramfinalproject.Models.ContactRequestModel; +import org.to.telegramfinalproject.Models.SearchRequestModel; +import org.to.telegramfinalproject.Models.SearchResultModel; + import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Scanner; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.to.telegramfinalproject.Models.ChatEntry; -import org.to.telegramfinalproject.Models.SearchRequestModel; +import java.util.*; public class ActionHandler { private final PrintWriter out; @@ -26,41 +26,43 @@ public class ActionHandler { public void loginHandler() { System.out.println("Login form: \n"); - System.out.println("Username: "); + System.out.print("Username: "); String username = this.scanner.nextLine(); - System.out.println("Password: "); + System.out.print("Password: "); String password = this.scanner.nextLine(); + JSONObject request = new JSONObject(); request.put("action", "login"); request.put("user_id", JSONObject.NULL); request.put("username", username); request.put("password", password); request.put("profile_name", JSONObject.NULL); + this.send(request); } public void register() { System.out.println("Register form: \n"); - System.out.println("Username: "); + System.out.print("Username: "); String username = this.scanner.nextLine(); - System.out.println("User id: "); + System.out.print("User id: "); String user_id = this.scanner.nextLine(); - System.out.println("Password: "); + System.out.print("Password: "); String password = this.scanner.nextLine(); - System.out.println("Profile name: "); + System.out.print("Profile name: "); String profile_name = this.scanner.nextLine(); + JSONObject request = new JSONObject(); request.put("action", "register"); request.put("user_id", user_id); request.put("username", username); request.put("password", password); request.put("profile_name", profile_name); + this.send(request); } - - public void search(){ - + public void search() { System.out.print("Enter keyword to search: "); String keyword = scanner.nextLine(); @@ -71,10 +73,27 @@ public class ActionHandler { String userId = Session.currentUser.getString("user_id"); SearchRequestModel model = new SearchRequestModel("search", keyword, userId); - send(model.toJson()); } + private void addContact(UUID contactId) { + JSONObject req = new JSONObject(); + req.put("action", "add_contact"); + req.put("user_id", Session.currentUser.getString("user_id")); + req.put("contact_id", contactId.toString()); + send(req); + } + + + private void joinGroupOrChannel(String type, String id) { + JSONObject req = new JSONObject(); + req.put("action", "join_" + type); + req.put("user_id", Session.currentUser.getString("user_id")); + req.put("id", id); + send(req); + } + + private void send(JSONObject request) { try { if (!request.has("action") || request.isNull("action")) { @@ -83,113 +102,122 @@ public class ActionHandler { } String action = request.getString("action"); - this.out.println(request.toString()); String responseText = this.in.readLine(); - - if (responseText != null) { - JSONObject response = new JSONObject(responseText); - System.out.println("Server response: " + response.getString("message")); - String status = response.getString("status"); - - if (status.equals("success") && response.has("data") && !response.isNull("data")) { - switch (action) { - case "login": - case "register": - Session.currentUser = response.getJSONObject("data"); - JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); - List chatList = new ArrayList<>(); - - for (Object obj : chatListJson) { - JSONObject chat = (JSONObject) obj; - ChatEntry entry = new ChatEntry( - chat.getString("id"), - chat.getString("name"), - chat.getString("image_url"), - chat.getString("type"), - chat.isNull("last_message_time") ? null : LocalDateTime.parse(chat.getString("last_message_time")) - ); - chatList.add(entry); - } - - Session.chatList = chatList; - break; - - case "search": - JSONArray results = response.getJSONObject("data").getJSONArray("results"); - - if (results.isEmpty()) { - System.out.println("No results found."); - } else { - System.out.println("\nSearch Results:"); - for (Object obj : results) { - JSONObject item = (JSONObject) obj; - if (item.getString("type").equals("message")) { - System.out.println("- [message] \"" + item.getString("content") + "\"" - + " (from: " + item.getString("sender") + ", at: " + item.getString("time") + ")"); - } else { - System.out.println("- [" + item.getString("type") + "] " - + item.getString("name") + " (ID: " + item.getString("id") + ")"); - } - - } - } - break; - - case "get_messages": - break; - } - } - - } else { + if (responseText == null) { System.out.println("No response from server."); + return; + } + + JSONObject response = new JSONObject(responseText); + System.out.println("Server response: " + response.getString("message")); + + String status = response.getString("status"); + if (!"success".equals(status) || !response.has("data") || response.isNull("data")) + return; + + switch (action) { + case "login": + case "register": + Session.currentUser = response.getJSONObject("data"); + JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); + List chatList = new ArrayList<>(); + + for (Object obj : chatListJson) { + JSONObject chat = (JSONObject) obj; + ChatEntry entry = new ChatEntry( + chat.getString("id"), + chat.getString("name"), + chat.getString("image_url"), + chat.getString("type"), + chat.isNull("last_message_time") ? null : + LocalDateTime.parse(chat.getString("last_message_time")) + ); + chatList.add(entry); + } + + Session.chatList = chatList; + break; + + case "search": + JSONArray results = response.getJSONObject("data").getJSONArray("results"); + + if (results.isEmpty()) { + System.out.println("No results found."); + } else { + System.out.println("\nSearch Results:"); + for (int i = 0; i < results.length(); i++) { + JSONObject item = results.getJSONObject(i); + String type = item.getString("type"); + if (type.equals("message")) { + System.out.println((i + 1) + ". [message] \"" + item.getString("content") + "\"" + + " (from: " + item.optString("sender", "N/A") + ", at: " + item.getString("time") + ")"); + } else { + System.out.println((i + 1) + ". [" + type + "] " + + item.getString("name") + " (ID: " + item.getString("id") + ")"); + } + } + + System.out.print("Select a result number to interact: "); + int index = Integer.parseInt(scanner.nextLine()) - 1; + if (index < 0 || index >= results.length()) return; + + JSONObject selected = results.getJSONObject(index); + String type = selected.getString("type"); + switch (type) { + case "user" -> { + UUID contactId = UUID.fromString(selected.getString("uuid")); // ✅ درست + addContact(contactId); + } + + case "group", "channel" -> { + joinGroupOrChannel(selected.getString("type"), selected.getString("uuid")); // ✅ + } + + default -> System.out.println("No interaction available for type: " + type); + } + } + break; + + + + case "get_messages": + // Optional: handle later + break; } } catch (IOException e) { - System.err.println("Error while communicating with server: " + e.getMessage()); + System.err.println("Error communicating with server: " + e.getMessage()); } catch (Exception e) { System.err.println("Client error: " + e.getMessage()); e.printStackTrace(); } } - - - - public void userMenu() { + public void userMenu(UUID internal_uuid) { while (true) { System.out.println("\nUser Menu:"); System.out.println("1. Show chat list"); System.out.println("2. Search"); - System.out.println("3. Logout"); - + System.out.println("3. Add contact"); + System.out.println("4. Logout"); System.out.print("Choose an option: "); String choice = scanner.nextLine(); switch (choice) { - - case "1": - showChatListAndSelect(); - break; - case "2" : - search(); - break; - - case "3": - + case "1" -> showChatListAndSelect(); + case "2" -> search(); + case "3" -> addContact(internal_uuid); + case "4" -> { logout(); - return; - default: - System.out.println("Invalid choice."); + } + default -> System.out.println("Invalid choice."); } } } - - - public void showChatListAndSelect() { if (Session.chatList == null || Session.chatList.isEmpty()) { System.out.println("No chats available."); @@ -199,8 +227,11 @@ public class ActionHandler { 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); + 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: "); @@ -215,24 +246,20 @@ public class ActionHandler { openChat(selected); } - private void openChat(ChatEntry chat) { JSONObject request = new JSONObject(); request.put("action", "get_messages"); request.put("receiver_id", chat.getId()); request.put("receiver_type", chat.getType()); - send(request); } - - public void logout() { if (Session.currentUser != null && Session.currentUser.has("user_id")) { JSONObject request = new JSONObject(); - String userId = Session.currentUser.getString("internalUUID"); request.put("action", "logout"); - request.put("user_id",userId); + request.put("user_id", Session.currentUser.getString("internalUUID")); + send(request); Session.currentUser = null; Session.chatList = null; @@ -241,5 +268,6 @@ public class ActionHandler { } } -} + +} diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java new file mode 100644 index 0000000..4de32f5 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -0,0 +1,86 @@ +package org.to.telegramfinalproject.Client; + + +import org.json.JSONObject; + +import java.io.BufferedReader; + +public class IncomingMessageListener implements Runnable { + private final BufferedReader in; + + public IncomingMessageListener(BufferedReader in) { + this.in = in; + } + + @Override + public void run() { + try { + String line; + while ((line = in.readLine()) != null) { + JSONObject response = new JSONObject(line); + + if (!response.has("action")) continue; + String action = response.getString("action"); + + switch (action) { + case "new_message" -> { + JSONObject msg = response.getJSONObject("data"); + 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.print(">> "); + } + + case "message_edited" -> { + JSONObject msg = response.getJSONObject("data"); + System.out.println("\n✏️ Message Edited:"); + System.out.println("ID: " + msg.getString("message_id")); + System.out.println("New Content: " + msg.getString("new_content")); + System.out.println("Edit Time: " + msg.getString("edit_time")); + System.out.print(">> "); + } + + case "message_deleted" -> { + JSONObject msg = response.getJSONObject("data"); + System.out.println("\n🗑️ Message Deleted:"); + System.out.println("Message ID: " + msg.getString("message_id")); + System.out.print(">> "); + } + + case "status_change" -> { + JSONObject msg = response.getJSONObject("data"); + System.out.println("\n🔄 User Status Changed:"); + System.out.println("User: " + msg.getString("user_id")); + System.out.println("Status: " + msg.getString("status")); + System.out.print(">> "); + } + + case "system_notification" -> { + JSONObject msg = response.getJSONObject("data"); + System.out.println("\n⚠️ System Notification:"); + System.out.println(msg.getString("content")); + System.out.print(">> "); + } + + case "contact_added" ->{ + System.out.println("\n🔔 You were added by a new contact: " + response.getString("user_id")); + System.out.print(">> "); + } + + + + default -> { + if (!action.equals("search")) { // ignore action: search + System.out.println("\n❓ Unknown action received: " + action); + System.out.print(">> "); + } + } + + } + } + } catch (Exception e) { + System.out.println("🔴 Listener stopped: " + e.getMessage()); + } + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 6897bfc..ba614c0 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -8,10 +8,11 @@ import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; +import java.util.UUID; public class TelegramClient { private static final String SERVER_HOST = "localhost"; - private static final int SERVER_PORT = 12345; + private static final int SERVER_PORT = 8000; private Socket socket; private BufferedReader in; private PrintWriter out; @@ -51,7 +52,11 @@ public class TelegramClient { this.handler.loginHandler(); if (Session.currentUser != null) { System.out.println("Login successful."); - this.handler.userMenu(); + UUID internalId = UUID.fromString(Session.currentUser.getString("internalUUID")); + //Thread listenerThread = new Thread(new IncomingMessageListener(in)); + //listenerThread.setDaemon(true); + //listenerThread.start(); + this.handler.userMenu(internalId); } else { System.out.println("Login failed."); diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java index 8f8de92..426c2c0 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java @@ -75,4 +75,115 @@ public class ChannelDatabase { } return result; } + + + + public static List getSubscriberUUIDs(UUID channelInternalUUID) { + List subscriberIds = new ArrayList<>(); + String sql = "SELECT user_id FROM channel_subscribe WHERE channel_id = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, channelInternalUUID); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + subscriberIds.add((UUID) rs.getObject("user_id")); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return subscriberIds; + } + + public static Channel findByChannelId(String channelId) { + String sql = "SELECT * FROM channels WHERE channel_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, channelId); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + Channel channel = new Channel(); + channel.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid"))); + channel.setChannel_id(rs.getString("channel_id")); + channel.setChannel_name(rs.getString("channel_name")); + channel.setImage_url(rs.getString("image_url")); + channel.setCreator_id(UUID.fromString(rs.getString("creator_id"))); + channel.setDescription(rs.getString("description")); + channel.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime()); + return channel; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + + + public static boolean isUserSubscribed(UUID userId, UUID channelInternalId) { + String sql = "SELECT * FROM channel_subscribe WHERE user_id = ? AND channel_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + stmt.setObject(2, channelInternalId); + ResultSet rs = stmt.executeQuery(); + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public static void addSubscriber(UUID channelInternalId, UUID userId) { + String sql = "INSERT INTO channel_subscribe (channel_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, channelInternalId); + stmt.setObject(2, userId); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public static UUID findInternalUUIDByChannelId(String channelId) { + String sql = "SELECT internal_uuid FROM channels WHERE channel_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, channelId); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return (UUID) rs.getObject("internal_uuid"); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + public static boolean addSubscriberToChannel(UUID userId, UUID channelUUID) { + String sql = """ + INSERT INTO channel_subscribers (channel_id, user_id) + VALUES (?, ?) + ON CONFLICT DO NOTHING + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, channelUUID); + stmt.setObject(2, userId); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java index 6ca06ed..a890995 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java @@ -16,13 +16,17 @@ public class ContactDatabase { return ConnectionDb.connect(); } + public static boolean addContact(UUID userId, UUID contactId) { + String sql = """ + INSERT INTO contacts (user_id, contact_id) + VALUES (?, ?) + ON CONFLICT DO NOTHING + """; - public boolean addContact(UUID user_id, UUID contact_id) { - String sql = "INSERT INTO contacts (user_id, contact_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; - try (Connection connection = getConnection()) { - PreparedStatement stmt = connection.prepareStatement(sql); - stmt.setObject(1, user_id); - stmt.setObject(2,contact_id); + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + stmt.setObject(2, contactId); return stmt.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); @@ -31,6 +35,7 @@ public class ContactDatabase { } + public boolean removeContact(UUID user_id, UUID contact_id) { String sql = "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?"; try (Connection connection = getConnection()) { @@ -94,7 +99,7 @@ public class ContactDatabase { } - public boolean existsContact(UUID user_id, UUID contact_id) { + public static boolean existsContact(UUID user_id, UUID contact_id) { String sql = "SELECT 1 FROM contacts WHERE user_id = ? AND contact_id = ? LIMIT 1"; // stop searching when find the first item in DB(LIMIT 1) try (Connection connection = getConnection()) { PreparedStatement stmt = connection.prepareStatement(sql); @@ -155,4 +160,6 @@ public class ContactDatabase { } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java index 79fa09b..b05abb5 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java @@ -76,5 +76,111 @@ public class GroupDatabase { return result; } + public static List getMemberUUIDs(UUID groupInternalUUID) { + List memberIds = new ArrayList<>(); + String sql = "SELECT user_id FROM group_members WHERE group_id = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, groupInternalUUID); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + memberIds.add((UUID) rs.getObject("user_id")); + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return memberIds; + } + + + + public static Group findByGroupId(String groupId) { + String sql = "SELECT * FROM groups WHERE group_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, groupId); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + Group group = new Group(); + group.setInternal_uuid(UUID.fromString(rs.getString("internal_uuid"))); + group.setGroup_id(rs.getString("group_id")); + group.setGroup_name(rs.getString("group_name")); + group.setImage_url(rs.getString("image_url")); + group.setCreator_id(UUID.fromString(rs.getString("creator_id"))); + group.setDescription(rs.getString("description")); + group.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime()); + return group; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + + public static boolean isUserInGroup(UUID userId, UUID groupInternalId) { + String sql = "SELECT * FROM group_members WHERE user_id = ? AND group_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, userId); + stmt.setObject(2, groupInternalId); + ResultSet rs = stmt.executeQuery(); + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + public static void addMember(UUID groupInternalId, UUID userId) { + String sql = "INSERT INTO group_members (group_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, groupInternalId); + stmt.setObject(2, userId); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + public static UUID findInternalUUIDByGroupId(String groupId) { + String sql = "SELECT internal_uuid FROM groups WHERE group_id = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, groupId); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return (UUID) rs.getObject("internal_uuid"); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + public static boolean addMemberToGroup(UUID userId, UUID groupUUID) { + String sql = """ + INSERT INTO group_members (group_id, user_id) + VALUES (?, ?) + ON CONFLICT DO NOTHING + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, groupUUID); + stmt.setObject(2, userId); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 3f959eb..957b61d 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -12,6 +12,41 @@ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = 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()); + + stmt.executeUpdate(); + + } catch (SQLException e) { + System.err.println("❌ Error saving message: " + e.getMessage()); + e.printStackTrace(); + } + } + 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/userDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java index 3427fa8..e5f09b1 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java @@ -228,7 +228,6 @@ public class userDatabase { } - public static User findByInternalUUID(UUID internalUuid) { String sql = "SELECT * FROM users WHERE internal_uuid = ?"; diff --git a/src/main/java/org/to/telegramfinalproject/Models/ContactRequestModel.java b/src/main/java/org/to/telegramfinalproject/Models/ContactRequestModel.java new file mode 100644 index 0000000..ac50e2d --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/ContactRequestModel.java @@ -0,0 +1,23 @@ +package org.to.telegramfinalproject.Models; + +import org.json.JSONObject; + +public class ContactRequestModel { + private String event; + private String contactId; + private String userId; + + public ContactRequestModel(String event, String contactId, String userId) { + this.event = event; + this.contactId = contactId; + this.userId = userId; + } + + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.put("event", event); + json.put("contact_id", contactId); + json.put("user_id", userId); + return json; + } +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java b/src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java new file mode 100644 index 0000000..036a801 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/SearchResultModel.java @@ -0,0 +1,59 @@ +package org.to.telegramfinalproject.Models; + +public class SearchResultModel { + private final String type; + private final String id; // ← UUID واقعی برای عملیات + private final String displayId; // ← user_id یا group_id برای نمایش + private String name; + private final String content; + private final String sender; + private final String time; + + public SearchResultModel(String type, String id, String displayId, + String content, String sender, String time) { + this.type = type; + this.id = id; + this.displayId = displayId; + this.content = content; + this.sender = sender; + this.time = time; + } + + // فقط در صورت نیاز برای user/group/channel + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public String getId() { + return id; + } + + public String getDisplayId() { + return displayId; + } + + public String getName() { + return name; + } + + public String getContent() { + return content; + } + + public String getSender() { + return sender; + } + + public String getTime() { + return time; + } + + @Override + public String toString() { + return "[" + type.toUpperCase() + "] " + name + " (ID: " + displayId + ")"; + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 230ff53..adabffe 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -69,20 +69,12 @@ public class ClientHandler implements Runnable { } else { User user = authService.login(request.getUsername(), request.getPassword()); - - if (user == null){ + if (user == null) { response = new ResponseModel("error", "Login failed."); break; } this.currentUser = user; - if (SessionManager.contains(user.getInternal_uuid())) { - response = new ResponseModel("error", "You are already logged in from another device."); - break; - - } - - SessionManager.addUser(user.getInternal_uuid(), this.socket); userDatabase.updateUserStatus(user.getInternal_uuid(), "online"); List contacts = ContactDatabase.getContacts(user.getInternal_uuid()); @@ -126,21 +118,16 @@ public class ClientHandler implements Runnable { case "logout": { String user_Id = requestJson.optString("user_id"); - if (user_Id != null && !user_Id.isEmpty()) { - try { - UUID uuid = UUID.fromString(user_Id); - userDatabase.updateUserStatus(uuid, "offline"); - userDatabase.updateLastSeen(uuid); - SessionManager.removeUser(uuid); - response = new ResponseModel("success", "Logged out."); - } catch (IllegalArgumentException ex) { - response = new ResponseModel("error", "Invalid UUID format for user_id."); - } + if (userId != null && !user_Id.isEmpty()) { + UUID uuid = UUID.fromString(user_Id); + userDatabase.updateUserStatus(uuid, "offline"); + userDatabase.updateLastSeen(uuid); + SessionManager.removeUser(uuid); + response = new ResponseModel("success", "Logged out."); } else { response = new ResponseModel("error", "Invalid user_id for logout."); } break; - } case "search": { @@ -154,6 +141,7 @@ public class ClientHandler implements Runnable { JSONObject obj = new JSONObject(); obj.put("type", "user"); obj.put("id", u.getUser_id()); + obj.put("uuid", u.getInternal_uuid().toString()); // ✅ اضافه شود obj.put("name", u.getProfile_name()); results.add(obj); } @@ -161,7 +149,8 @@ public class ClientHandler implements Runnable { for (Group g : GroupDatabase.searchGroups(keyword)) { JSONObject obj = new JSONObject(); obj.put("type", "group"); - obj.put("id", g.getGroup_id()); + obj.put("id", g.getGroup_id()); // قابل نمایش + obj.put("uuid", g.getInternal_uuid().toString()); // برای عملیات obj.put("name", g.getGroup_name()); results.add(obj); } @@ -169,7 +158,8 @@ public class ClientHandler implements Runnable { for (Channel c : ChannelDatabase.searchChannels(keyword)) { JSONObject obj = new JSONObject(); obj.put("type", "channel"); - obj.put("id", c.getChannel_id()); + obj.put("id", c.getChannel_id()); // قابل نمایش + obj.put("uuid", c.getInternal_uuid().toString()); // برای عملیات obj.put("name", c.getChannel_name()); results.add(obj); } @@ -224,6 +214,40 @@ public class ClientHandler implements Runnable { break; } + case "add_contact": { + UUID userUUID = new userDatabase().findByUserId(requestJson.getString("user_id")).getInternal_uuid(); + UUID contactUUID = UUID.fromString(requestJson.getString("contact_id")); + + boolean success = ContactDatabase.addContact(userUUID, contactUUID); + response = success + ? new ResponseModel("success", "Contact added successfully.") + : new ResponseModel("error", "Failed to add contact. Maybe already exists."); + break; + } + + case "join_group": { + UUID userUUID = new userDatabase().findByUserId(requestJson.getString("user_id")).getInternal_uuid(); + UUID groupUUID = GroupDatabase.findInternalUUIDByGroupId(requestJson.getString("id")); + + boolean joined = GroupDatabase.addMemberToGroup(userUUID, groupUUID); + response = joined + ? new ResponseModel("success", "Joined group.") + : new ResponseModel("error", "Failed to join group."); + break; + } + + case "join_channel": { + UUID userUUID = new userDatabase().findByUserId(requestJson.getString("user_id")).getInternal_uuid(); + UUID channelUUID = ChannelDatabase.findInternalUUIDByChannelId(requestJson.getString("id")); + + boolean joined = ChannelDatabase.addSubscriberToChannel(userUUID, channelUUID); + response = joined + ? new ResponseModel("success", "Joined channel.") + : new ResponseModel("error", "Failed to join channel."); + break; + } + + default: response = new ResponseModel("error", "Unknown action: " + action); } @@ -236,7 +260,7 @@ public class ClientHandler implements Runnable { } } catch (IOException e) { System.out.println("Connection with client lost."); - userId = (currentUser != null) ? currentUser.getInternal_uuid() : SessionManager.getUserIdBySocket(this.socket); + userId = (currentUser != null) ? currentUser.getInternal_uuid() : SessionManager.getUserIdBySocket(this.socket); if (userId != null) { userDatabase.updateUserStatus(userId, "offline"); userDatabase.updateLastSeen(userId); @@ -261,4 +285,4 @@ public class ClientHandler implements Runnable { } -} +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Server/ContactService.java b/src/main/java/org/to/telegramfinalproject/Server/ContactService.java new file mode 100644 index 0000000..1c00835 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/ContactService.java @@ -0,0 +1,15 @@ +package org.to.telegramfinalproject.Server; + +import java.util.UUID; +import org.to.telegramfinalproject.Database.userDatabase; +import org.to.telegramfinalproject.Database.ContactDatabase; +public class ContactService { + public static boolean addContact(UUID userId, UUID contactId) { + if (userId.equals(contactId)) return false; + if (userDatabase.findByInternalUUID(contactId) == null) return false; + if (userDatabase.findByInternalUUID(userId) == null) return false; + if (ContactDatabase.existsContact(userId, contactId)) return false; + + return ContactDatabase.addContact(userId, contactId); + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Server/MainServer.java b/src/main/java/org/to/telegramfinalproject/Server/MainServer.java index 5de2fbd..2318fa3 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/MainServer.java +++ b/src/main/java/org/to/telegramfinalproject/Server/MainServer.java @@ -7,7 +7,7 @@ import java.net.ServerSocket; import java.net.Socket; public class MainServer { - private static final int PORT = 12345; + private static final int PORT = 8000; public static void main(String[] args) { try (ServerSocket serverSocket = new ServerSocket(PORT)) { diff --git a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java new file mode 100644 index 0000000..d2a341f --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -0,0 +1,248 @@ +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.Models.Message; +import org.to.telegramfinalproject.Models.User; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public class RealTimeEventDispatcher { + + public static void sendToUser(UUID userId, JSONObject data) { + Socket socket = SessionManager.getUserSocket(userId); + + + if (socket != null && !socket.isClosed()) { + try { + + + PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + out.println(data.toString()); + } catch (IOException e) { + System.err.println("❌ Error sending to user: " + e.getMessage()); + } + + } + else { + System.out.println("⚠️ User " + userId + " is offline. Skipping real-time send."); + + } + } + + public static void broadcastToUsers(List userIds, JSONObject data) { + for (UUID userId : userIds) { + sendToUser(userId, data); + } + } + + public static JSONObject buildEvent(String action, JSONObject payload) { + JSONObject json = new JSONObject(); + json.put("action", action); + json.put("data", payload); + return json; + } + + + public static void notifyNewMessage(Message msg, User sender) { + JSONObject data = new JSONObject(); + data.put("sender", sender.getUser_id()); + data.put("receiver_type", msg.getReceiver_type()); + data.put("receiver_id", msg.getReceiver_id()); + data.put("content", msg.getContent()); + data.put("time", msg.getSend_at().toString()); + + JSONObject event = buildEvent("new_message", data); + + switch (msg.getReceiver_type()) { + case "private" -> RealTimeEventDispatcher.sendToUser(msg.getReceiver_id(), event); + case "group" -> { + List memberIds = GroupDatabase.getMemberUUIDs(msg.getReceiver_id()); + memberIds.remove(sender.getInternal_uuid()); + RealTimeEventDispatcher.broadcastToUsers(memberIds, event); + } + case "channel" -> { + List subscriberIds = ChannelDatabase.getSubscriberUUIDs(msg.getReceiver_id()); + subscriberIds.remove(sender.getInternal_uuid()); + subscriberIds.remove(sender.getInternal_uuid()); + RealTimeEventDispatcher.broadcastToUsers(subscriberIds, event); + } + } + } + + + + public static void notifyMessageEdited(UUID messageId, String newContent, List receivers) { + + String editTime = LocalDateTime.now().toString(); + JSONObject data = new JSONObject(); + data.put("message_id", messageId.toString()); + data.put("new_content", newContent); + data.put("edited_at", editTime); + + JSONObject event = new JSONObject(); + event.put("action", "edit_message"); + event.put("data", data); + + broadcastToUsers(receivers, event); + } + + + public static void notifyMessageDeleted(UUID messageId, List receivers) { + JSONObject data = new JSONObject(); + data.put("message_id", messageId.toString()); + + JSONObject event = new JSONObject(); + event.put("action", "delete_message"); + event.put("data", data); + + broadcastToUsers(receivers, event); + } + + public static void notifyUserUpdated(UUID userId, String newProfileName, String newImageUrl, List contactIds) { + JSONObject data = new JSONObject(); + data.put("user_id", userId.toString()); + data.put("new_name", newProfileName); + data.put("new_image_url", newImageUrl); + + JSONObject event = new JSONObject(); + event.put("action", "update_user"); + event.put("data", data); + + broadcastToUsers(contactIds, event); + } + + public static void notifyChatDeleted(String type, UUID id, List affectedUsers) { + JSONObject data = new JSONObject(); + data.put("chat_type", type); // private, group, channel + data.put("chat_id", id.toString()); + + JSONObject event = new JSONObject(); + event.put("action", "chat_deleted"); + event.put("data", data); + + broadcastToUsers(affectedUsers, event); + } + + public static void notifyGroupOrChannelUpdated(String type, UUID id, String newName, String newImageUrl, List affectedUsers) { + JSONObject data = new JSONObject(); + data.put("chat_type", type); // "group" or "channel" + data.put("chat_id", id.toString()); + data.put("new_name", newName); + data.put("new_image_url", newImageUrl); + + JSONObject event = new JSONObject(); + event.put("action", "update_group_or_channel"); + event.put("data", data); + + broadcastToUsers(affectedUsers, event); + } + + public static void notifyMediaMessage(Message msg, User sender) { + JSONObject data = new JSONObject(); + 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()); + + JSONObject event = new JSONObject(); + event.put("action", "new_media"); + event.put("data", data); + + switch (msg.getReceiver_type()) { + case "private" -> sendToUser(msg.getReceiver_id(), event); + case "group" -> { + List members = GroupDatabase.getMemberUUIDs(msg.getReceiver_id()); + members.remove(sender.getInternal_uuid()); + broadcastToUsers(members, event); + } + case "channel" -> { + List subs = ChannelDatabase.getSubscriberUUIDs(msg.getReceiver_id()); + subs.remove(sender.getInternal_uuid()); + broadcastToUsers(subs, event); + } + } + } + + public static void notifyAddedToChat(String type, UUID chatId, String chatName, String imageUrl, UUID userId) { + JSONObject data = new JSONObject(); + data.put("chat_type", type); // group یا channel + data.put("chat_id", chatId.toString()); + data.put("chat_name", chatName); + data.put("image_url", imageUrl); + + JSONObject event = new JSONObject(); + event.put("action", type.equals("group") ? "added_to_group" : "added_to_channel"); + event.put("data", data); + + sendToUser(userId, event); + } + + public static void notifyRemovedFromChat(String type, UUID chatId, UUID userId) { + JSONObject data = new JSONObject(); + data.put("chat_type", type); + data.put("chat_id", chatId.toString()); + + JSONObject event = new JSONObject(); + event.put("action", type.equals("group") ? "removed_from_group" : "removed_from_channel"); + event.put("data", data); + + sendToUser(userId, event); + } + public static void notifyMessageSeen(UUID messageId, UUID senderId) { + JSONObject data = new JSONObject(); + data.put("message_id", messageId.toString()); + data.put("seen_at", LocalDateTime.now().toString()); + + JSONObject event = new JSONObject(); + event.put("action", "message_seen"); + event.put("data", data); + + sendToUser(senderId, event); + } + + + public static void notifyBlocked(UUID blockerId, UUID blockedUserId) { + JSONObject data = new JSONObject(); + data.put("blocker_id", blockerId.toString()); + + JSONObject event = new JSONObject(); + event.put("action", "blocked_by_user"); + event.put("data", data); + + sendToUser(blockedUserId, event); + } + + + public static void notifyUnblocked(UUID unblockerId, UUID unblockedUserId) { + JSONObject data = new JSONObject(); + data.put("unblocker_id", unblockerId.toString()); + + JSONObject event = new JSONObject(); + event.put("action", "unblocked_by_user"); + event.put("data", data); + + sendToUser(unblockedUserId, event); + } + + public static void notifyUserStatusChanged(UUID userId, String status, List contacts) { + JSONObject data = new JSONObject(); + data.put("user_id", userId.toString()); + data.put("status", status); // online | offline + data.put("time", LocalDateTime.now().toString()); + + JSONObject event = buildEvent("user_status_changed", data); + + broadcastToUsers(contacts, event); + } + + +} diff --git a/src/main/resources/org/to/telegramfinalproject/Contact_Cell.fxml b/src/main/resources/org/to/telegramfinalproject/Contact_Cell.fxml new file mode 100644 index 0000000..7dd1d98 --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/Contact_Cell.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/to/telegramfinalproject/MainPage.fxml b/src/main/resources/org/to/telegramfinalproject/MainPage.fxml new file mode 100644 index 0000000..a637fda --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/MainPage.fxml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file