diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 5f9d192..e79cc9f 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -3,8 +3,14 @@ package org.to.telegramfinalproject.Client; 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; public class ActionHandler { private final PrintWriter out; @@ -51,20 +57,146 @@ public class ActionHandler { this.send(request); } + + public void search(){ + + System.out.print("Enter keyword to search: "); + String keyword = scanner.nextLine(); + + JSONObject request = new JSONObject(); + request.put("action", "search"); + request.put("keyword", keyword); + + send(request); + + } + private void send(JSONObject request) { try { 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"); + + String action = request.getString("action"); + + if (status.equals("success") && response.has("data") && !response.isNull("data")) { + if (action.equals("login") || action.equals("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; + } + + else if (action.equals("search")) { + + JSONArray results = response.getJSONObject("data").getJSONArray("results"); + + System.out.println("\nSearch Results:"); + for (Object obj : results) { + JSONObject item = (JSONObject) obj; + System.out.println("- [" + item.getString("type") + "] " + item.getString("name") + " (ID: " + item.getString("id") + ")"); + } + } + } } else { System.out.println("No response from server."); } + } catch (IOException e) { System.err.println("Error: " + e.getMessage()); } - } + + + + public void userMenu() { + 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.print("Choose an option: "); + String choice = scanner.nextLine(); + + switch (choice) { + + case "1": + showChatListAndSelect(); + break; + case "2" : + search(); + break; + + case "3": + Session.currentUser = null; + Session.chatList = null; + System.out.println("Logged out."); + return; + default: + System.out.println("Invalid choice."); + } + } + } + + + + + 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 < 0 || choice >= Session.chatList.size()) { + System.out.println("Invalid selection."); + return; + } + + ChatEntry selected = Session.chatList.get(choice); + 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); + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java new file mode 100644 index 0000000..9bf0d73 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -0,0 +1,13 @@ +package org.to.telegramfinalproject.Client; + +import org.json.JSONObject; +import org.to.telegramfinalproject.Models.ChatEntry; + +import java.util.List; + + +// method for save data from server response +public class Session { + public static JSONObject currentUser; + public static List chatList; +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 686c02d..bd56550 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -47,6 +47,13 @@ public class TelegramClient { break; case "2": this.handler.loginHandler(); + if (Session.currentUser != null) { + System.out.println("Login successful."); + this.handler.userMenu(); + + } else { + System.out.println("Login failed."); + } break; case "3": System.out.println(" Disconnecting..."); diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java new file mode 100644 index 0000000..e2b0754 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java @@ -0,0 +1,76 @@ +package org.to.telegramfinalproject.Database; + +import org.to.telegramfinalproject.Models.Channel; +import org.to.telegramfinalproject.Models.Group; + +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 ChannelDatabase { + public static List getChannelsByUser(UUID internalUuid) { + List channels = new ArrayList<>(); + + String sql = """ + SELECT c.* FROM channels c + JOIN channel_subscribers cs ON c.internal_uuid = cs.channel_id + WHERE cs.user_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setObject(1, internalUuid); + ResultSet rs = stmt.executeQuery(); + + while (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.setCreator_id(UUID.fromString(rs.getString("creator_id"))); + channel.setImage_url(rs.getString("image_url")); + channel.setDescription(rs.getString("description")); + channel.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime()); + + channels.add(channel); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return channels; + } + + + public static List searchChannels(String keyword) { + List result = new ArrayList<>(); + String sql = "SELECT * FROM channels WHERE channel_name ILIKE ? OR channel_id ILIKE ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, "%" + keyword + "%"); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Channel channel = new Channel( + UUID.fromString(rs.getString("internal_uuid")), + rs.getString("channel_name"), + UUID.fromString(rs.getString("creator_id")), + rs.getTimestamp("created_at").toLocalDateTime() + ); + channel.setChannel_id(rs.getString("channel_id")); + channel.setImage_url(rs.getString("image_url")); + channel.setDescription(rs.getString("description")); + result.add(channel); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java new file mode 100644 index 0000000..6ca06ed --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java @@ -0,0 +1,158 @@ +package org.to.telegramfinalproject.Database; + +import org.to.telegramfinalproject.Models.Contact; +import org.to.telegramfinalproject.Models.User; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ContactDatabase { + public ContactDatabase(){} + + + private static Connection getConnection() throws SQLException { + return ConnectionDb.connect(); + } + + + 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); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + + public boolean removeContact(UUID user_id, UUID contact_id) { + String sql = "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?"; + try (Connection connection = getConnection()) { + PreparedStatement stmt = connection.prepareStatement(sql); + stmt.setObject(1, user_id); + stmt.setObject(2, contact_id); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public boolean blockContact(UUID user_id, UUID contact_id) { + String sql = "UPDATE contacts SET is_blocked = TRUE WHERE user_id = ? AND contact_id = ?"; + try (Connection connection = getConnection()) { + PreparedStatement stmt = connection.prepareStatement(sql); + stmt.setObject(1, user_id); + stmt.setObject(2, contact_id); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public boolean unblockContact(UUID user_id, UUID contact_id) { + String sql = "UPDATE contacts SET is_blocked = FALSE WHERE user_id = ? AND contact_id = ?"; + try (Connection connection = getConnection()) { + PreparedStatement stmt = connection.prepareStatement(sql); + stmt.setObject(1, user_id); + stmt.setObject(2, contact_id); + return stmt.executeUpdate() > 0; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + public static List getContacts(UUID user_id) { + List contacts = new ArrayList<>(); + String sql = "SELECT * FROM contacts WHERE user_id = ?"; + try (Connection connection = getConnection()) { + PreparedStatement stmt = connection.prepareStatement(sql); + stmt.setObject(1, user_id); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + UUID contactId = (UUID) rs.getObject("contact_id"); + boolean isBlocked = rs.getBoolean("is_blocked"); + Timestamp addedAt = rs.getTimestamp("added_at"); + + Contact contact = new Contact(user_id, contactId); + contact.setIs_blocked(isBlocked); + contact.setAdd_at(addedAt.toLocalDateTime()); + contacts.add(contact); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return contacts; + } + + + public 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); + stmt.setObject(1, user_id); + stmt.setObject(2, contact_id); + ResultSet rs = stmt.executeQuery(); + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + + public boolean isBlocked(UUID user_id, UUID contact_id) { + String sql = "SELECT is_blocked FROM contacts WHERE user_id = ? AND contact_id = ?"; + try (Connection connection = getConnection()) { + PreparedStatement stmt = connection.prepareStatement(sql); + stmt.setObject(1, user_id); + stmt.setObject(2, contact_id); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + return rs.getBoolean("is_blocked"); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public List searchContacts(UUID user_id, String searchTerm) { + List results = new ArrayList<>(); //ILIKE case-insensitive + String sql = """ + SELECT c.contact_id, c.added_at, c.is_blocked + FROM contacts c + JOIN users u ON c.contact_id = u.internal_uuid + WHERE c.user_id = ? AND (u.user_id ILIKE ? OR u.profile_name ILIKE ?) + """; + try (Connection connection = getConnection()) { + PreparedStatement stmt = connection.prepareStatement(sql); + stmt.setObject(1, user_id); + stmt.setString(2, "%" + searchTerm + "%"); + stmt.setString(3, "%" + searchTerm + "%"); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + UUID contactId = (UUID) rs.getObject("contact_id"); + boolean isBlocked = rs.getBoolean("is_blocked"); + Timestamp addedAt = rs.getTimestamp("added_at"); + Contact contact = new Contact(user_id, contactId); + contact.setIs_blocked(isBlocked); + contact.setAdd_at(addedAt.toLocalDateTime()); + results.add(contact); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return results; + } + + +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java new file mode 100644 index 0000000..722f266 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/GroupDatabase.java @@ -0,0 +1,78 @@ +package org.to.telegramfinalproject.Database; + +import org.to.telegramfinalproject.Models.Group; + +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 GroupDatabase { + + public static List getGroupsByUser(UUID internalUuid) { + List groups = new ArrayList<>(); + + String sql = """ + SELECT g.* FROM groups g + JOIN group_members gm ON g.internal_uuid = gm.group_id + WHERE gm.user_id = ? + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setObject(1, internalUuid); + ResultSet rs = stmt.executeQuery(); + + while (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.setCreator_id(UUID.fromString(rs.getString("creator_id"))); + group.setImage_url(rs.getString("image_url")); + group.setDescription(rs.getString("description")); + group.setCreated_at(rs.getTimestamp("created_at").toLocalDateTime()); + + groups.add(group); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return groups; + } + + public static List searchGroups(String keyword) { + List result = new ArrayList<>(); + String sql = "SELECT * FROM groups WHERE group_name ILIKE ? OR group_id ILIKE ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, "%" + keyword + "%"); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + Group group = new Group( + UUID.fromString(rs.getString("internal_uuid")), + rs.getString("group_name"), + UUID.fromString(rs.getString("creator_id")), + rs.getTimestamp("created_at").toLocalDateTime() + ); + group.setGroup_id(rs.getString("group_id")); + group.setImage_url(rs.getString("image_url")); + group.setDescription(rs.getString("description")); + result.add(group); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } + + +} \ 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 new file mode 100644 index 0000000..9b5a814 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -0,0 +1,135 @@ +package org.to.telegramfinalproject.Database; + +import org.to.telegramfinalproject.Models.Message; + +import java.sql.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class MessageDatabase { + + + 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(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, messageId); + stmt.setObject(2, userId); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + + + + public static List getUnreadMessages(UUID userId) { + List messages = new ArrayList<>(); + + String sql = """ + SELECT m.* FROM messages m + LEFT JOIN message_receipts r ON m.message_id = r.message_id AND r.user_id = ? + WHERE r.user_id IS NULL + AND ( + (m.receiver_type = 'private' AND m.receiver_id = ?) + OR + (m.receiver_type = 'group' AND EXISTS ( + SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ? + )) + OR + (m.receiver_type = 'channel' AND EXISTS ( + SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ? + )) + ) + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setObject(1, userId); + stmt.setObject(2, userId); + stmt.setObject(3, userId); + stmt.setObject(4, userId); + + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + messages.add(new Message( + UUID.fromString(rs.getString("message_id")), + rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, + rs.getString("receiver_type"), + UUID.fromString(rs.getString("receiver_id")), + rs.getString("content"), + rs.getString("message_type"), + rs.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, + rs.getBoolean("is_edited"), + rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null, + rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null, + rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null + )); + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return messages; + } + + + public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) { + String sql = """ + SELECT MAX(send_at) FROM messages + WHERE receiver_type = ? + AND ( + (sender_id = ? AND receiver_id = ?) + OR (sender_id = ? AND receiver_id = ?) + ) + """; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, type); + stmt.setObject(2, user1); + stmt.setObject(3, user2); + stmt.setObject(4, user2); + stmt.setObject(5, user1); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + Timestamp ts = rs.getTimestamp(1); + return ts != null ? ts.toLocalDateTime() : null; + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return null; + } + + + public static LocalDateTime getLastMessageTime(UUID receiverId, String type) { + String sql = "SELECT MAX(send_at) FROM messages WHERE receiver_id = ? AND receiver_type = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, receiverId); + stmt.setString(2, type); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + Timestamp ts = rs.getTimestamp(1); + return ts != null ? ts.toLocalDateTime() : null; + } + } catch (SQLException e) { + e.printStackTrace(); + } + + return null; + } + +} diff --git a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java index b89596e..8488fda 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/userDatabase.java @@ -3,10 +3,7 @@ package org.to.telegramfinalproject.Database; import org.to.telegramfinalproject.Models.User; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; +import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -203,5 +200,87 @@ public class userDatabase { return false; } } + + + public static void updateUserStatus(UUID uuid, String status) { + String sql = "UPDATE users SET status = ? WHERE internal_uuid = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, status); + stmt.setObject(2, uuid); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public static void updateLastSeen(UUID uuid) { + String sql = "UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE internal_uuid = ?"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, uuid); + stmt.executeUpdate(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + + public static User findByInternalUUID(UUID internalUuid) { + String sql = "SELECT * FROM users WHERE internal_uuid = ?"; + + try (Connection conn = ConnectionDb.connect(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setObject(1, internalUuid); + ResultSet rs = stmt.executeQuery(); + + if (rs.next()) { + User user = new User( + rs.getString("user_id"), + UUID.fromString(rs.getString("internal_uuid")), + rs.getString("username"), + rs.getString("password"), + rs.getString("profile_name") + ); + + user.setBio(rs.getString("bio")); + user.setImage_url(rs.getString("image_url")); + user.setStatus(rs.getString("status")); + + Timestamp lastSeenTs = rs.getTimestamp("last_seen"); + if (lastSeenTs != null) { + user.setLast_seen(lastSeenTs.toLocalDateTime()); + } + + return user; + } + + } catch (SQLException e) { + e.printStackTrace(); + } + + return null; + } + + public List searchUsers(String keyword) { + String query = "SELECT * FROM users WHERE user_id ILIKE ? OR profile_name ILIKE ?"; //(ILIKE) case_insensitive + List result = new ArrayList<>(); + try (Connection conn = getConnection(); + PreparedStatement stmt = conn.prepareStatement(query)) { + stmt.setString(1, "%" + keyword + "%"); + stmt.setString(2, "%" + keyword + "%"); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + result.add(extractUser(rs)); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return result; + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Channel.java b/src/main/java/org/to/telegramfinalproject/Models/Channel.java index eff1738..8965066 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Channel.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Channel.java @@ -21,6 +21,10 @@ public class Channel { this.created_at = created_at; } + public Channel() { + + } + public void setChannel_id(String Channel_id){this.channel_id = Channel_id;} public void setCreator_id(UUID creator_id){this.creator_id = creator_id;} public void setChannel_name(String channel_name){this.channel_name = channel_name;} @@ -40,4 +44,6 @@ public class Channel { return this.internal_uuid; } + public void setInternal_uuid(UUID internalUuid) { this.internal_uuid = internalUuid; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChannelSubscribe.java b/src/main/java/org/to/telegramfinalproject/Models/ChannelSubscribe.java index 12bfe5b..1950890 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ChannelSubscribe.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ChannelSubscribe.java @@ -3,27 +3,27 @@ package org.to.telegramfinalproject.Models; import java.time.LocalDateTime; import java.util.UUID; -public class ChannelSubscribe { +public class ChannelSubscribe{ private UUID channel_id; private UUID user_id; - private LocalDateTime join_at; + private LocalDateTime Subscribed_at; public ChannelSubscribe(UUID channel_id, UUID user_id, String role){ this.channel_id = channel_id; this.user_id = user_id; - this.join_at = LocalDateTime.now(); + this.Subscribed_at = LocalDateTime.now(); } public void setChannel_id(UUID group_id){this.channel_id = group_id;} public void setUser_id(UUID user_id){this.user_id = user_id;} - public void setJoin_at(LocalDateTime join_at){this.join_at = join_at;} + public void setJoin_at(LocalDateTime join_at){this.Subscribed_at = join_at;} public UUID getChannel_id(){return this.channel_id;} public UUID getUser_id(){return this.user_id;} - public LocalDateTime getJoin_at(){return this.join_at;} + public LocalDateTime getJoin_at(){return this.Subscribed_at;} } diff --git a/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java new file mode 100644 index 0000000..52d3916 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/ChatEntry.java @@ -0,0 +1,39 @@ +package org.to.telegramfinalproject.Models; + +import java.time.LocalDateTime; + +public class ChatEntry { + private final String name; + private final String id; + private final String imageUrl; + private final String type; // "private", "group", "channel" + private final LocalDateTime lastMessageTime; + + public ChatEntry(String name, String id, String imageUrl, String type, LocalDateTime lastMessageTime) { + this.name = name; + this.id = id; + this.imageUrl = imageUrl; + this.type = type; + this.lastMessageTime = lastMessageTime; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + public String getImageUrl() { + return imageUrl; + } + + public String getType() { + return type; + } + + public LocalDateTime getLastMessageTime() { + return lastMessageTime; + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/Contact.java b/src/main/java/org/to/telegramfinalproject/Models/Contact.java index d6c5f66..8e78344 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Contact.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Contact.java @@ -1,23 +1,27 @@ package org.to.telegramfinalproject.Models; +import org.json.JSONArray; +import org.json.JSONObject; + import java.time.LocalDateTime; +import java.util.List; import java.util.UUID; public class Contact { private UUID user_id; private UUID contact_id; - private LocalDateTime add_at; + private LocalDateTime added_at; private Boolean is_blocked; public Contact(UUID user_id, UUID contact_id){ this.user_id = user_id; this.contact_id = contact_id; - this.add_at = LocalDateTime.now(); + this.added_at = LocalDateTime.now(); } public void setUser_id(UUID user_id){this.user_id = user_id;} public void setContact_id(UUID contact_id){this.contact_id = contact_id;} - public void setAdd_at(LocalDateTime add_at){this.add_at =add_at;} + public void setAdd_at(LocalDateTime add_at){this.added_at =add_at;} public void setIs_blocked(Boolean is_blocked){this.is_blocked =is_blocked;} public UUID getUser_id(){ @@ -27,9 +31,14 @@ public class Contact { public UUID getContact_id(){return this.contact_id;} public LocalDateTime getAdd_at() { - return add_at; + return added_at; } public Boolean getIs_blocked(){ return is_blocked; } + + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Group.java b/src/main/java/org/to/telegramfinalproject/Models/Group.java index 28ed2ed..922c725 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Group.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Group.java @@ -21,6 +21,10 @@ public class Group { this.created_at = created_at; } + public Group() { + + } + public void setGroup_id(String group_id){this.group_id = group_id;} public void setCreator_id(UUID creator_id){this.creator_id = creator_id;} public void setGroup_name(String group_name){this.group_name = group_name;} @@ -40,4 +44,6 @@ public class Group { return this.internal_uuid; } + public void setInternal_uuid(UUID internalUuid) { this.internal_uuid = internalUuid; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java new file mode 100644 index 0000000..4e3a37f --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/JsonUtil.java @@ -0,0 +1,157 @@ +package org.to.telegramfinalproject.Models; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public class JsonUtil { + public static JSONArray contactListToJson(List contacts) { + + + JSONArray array = new JSONArray(); + for (Contact contact : contacts) { + JSONObject obj = new JSONObject(); + obj.put("user_id", contact.getUser_id().toString()); + obj.put("contact_id", contact.getContact_id().toString()); + obj.put("is_blocked", contact.getIs_blocked()); + obj.put("added_at", contact.getAdd_at().toString()); + array.put(obj); + } + return array; + } + + + + + public static JSONObject userToJson(User user) { + JSONObject obj = new JSONObject(); + obj.put("internalUUID", user.getInternal_uuid().toString()); + obj.put("user_id", user.getUser_id() != null ?user.getUser_id().toString() :JSONObject.NULL); + obj.put("username", user.getUsername()); + obj.put("profile_name", user.getProfile_name()); + obj.put("bio", user.getBio()!= null ?user.getBio().toString() :JSONObject.NULL); + obj.put("image_url", user.getImage_url() != null ?user.getImage_url().toString() :JSONObject.NULL); + obj.put("status", user.getStatus()); + obj.put("last_seen", user.getLast_seen() != null ? user.getLast_seen().toString() : JSONObject.NULL); + obj.put("contactList", JsonUtil.contactListToJson(user.getContactList())); + obj.put("channelList", JsonUtil.channelListToJson(user.getChannelList())); + obj.put("groupList", JsonUtil.groupListToJson(user.getGroupList())); + obj.put("unreadMessages", JsonUtil.messageListToJson(user.getUnreadMessages())); + return obj; + } + + + public static JSONArray messageListToJson(List messages) { + JSONArray array = new JSONArray(); + for (Message message : messages) { + JSONObject obj = new JSONObject(); + obj.put("message_id", message.getMessage_id().toString()); + obj.put("sender_id", message.getSender_id() != null ? message.getSender_id().toString() : JSONObject.NULL); + obj.put("receiver_type", message.getReceiver_type()); + 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); + obj.put("is_edited", message.isIs_edited()); + obj.put("original_message_id", message.getOriginal_message_id() != null ? message.getOriginal_message_id().toString() : JSONObject.NULL); + obj.put("forwarded_by", message.getForwarded_by() != null ? message.getForwarded_by().toString() : JSONObject.NULL); + obj.put("forwarded_from", message.getForwarded_from() != null ? message.getForwarded_from().toString() : JSONObject.NULL); + array.put(obj); + } + return array; + } + + + + + public static JSONArray groupListToJson(List groups) { + JSONArray array = new JSONArray(); + for (Group group : groups) { + JSONObject obj = new JSONObject(); + obj.put("internal_uuid", group.getInternal_uuid().toString()); + obj.put("group_id", group.getGroup_id() != null ?group.getGroup_id().toString() :JSONObject.NULL); + obj.put("group_name", group.getGroup_name()); + obj.put("creator_id", group.getCreator_id().toString()); + obj.put("image_url",group.getImage_url()!= null ?group.getImage_url().toString() : JSONObject.NULL ); + obj.put("description", group.getDescription() != null ?group.getDescription().toString() : JSONObject.NULL ); + obj.put("created_at", group.getCreated_at().toString()); + obj.put("members", JsonUtil.groupMemberListToJson(group.getMembers())); + array.put(obj); + } + return array; + } + + + + + public static JSONArray channelListToJson(List channels) { + JSONArray array = new JSONArray(); + for (Channel channel : channels) { + JSONObject obj = new JSONObject(); + obj.put("internal_uuid",channel.getInternal_uuid().toString()); + obj.put("channel_id", channel.getChannel_id() != null ?channel.getChannel_id().toString() :JSONObject.NULL); + obj.put("channel_name", channel.getChannel_name()); + obj.put("creator_id", channel.getCreator_id().toString()); + obj.put("image_url",channel.getImage_url()!= null ?channel.getImage_url().toString() : JSONObject.NULL ); + obj.put("description",channel.getDescription() != null ?channel.getDescription().toString() : JSONObject.NULL ); + obj.put("created_at",channel.getCreated_at().toString()); + obj.put("members", JsonUtil.channelSubscribeToJson(channel.getMembers())); + array.put(obj); + } + return array; + } + + + + + public static JSONArray groupMemberListToJson(List members) { + JSONArray array = new JSONArray(); + for (GroupMember m : members) { + JSONObject obj = new JSONObject(); + obj.put("group_id", m.getGroup_id().toString()); + obj.put("user_id", m.getUser_id().toString()); + obj.put("joined_at", m.getJoin_at().toString()); + obj.put("role", m.getRole()); + array.put(obj); + } + return array; + } + + public static JSONArray channelSubscribeToJson(List subscribes){ + JSONArray array = new JSONArray(); + for(ChannelSubscribe s :subscribes ){ + JSONObject obj = new JSONObject(); + obj.put("channel_id",s.getChannel_id().toString()); + obj.put("user_id", s.getUser_id().toString()); + obj.put("Subscribed_at", s.getJoin_at().toString()); + } + + return array; + } + + + public static JSONArray chatListToJson(List chatList) { + JSONArray jsonArray = new JSONArray(); + + for (ChatEntry entry : chatList) { + JSONObject obj = new JSONObject(); + obj.put("id", entry.getId() != null ?entry.getId() :JSONObject.NULL); + obj.put("name", entry.getName()); + obj.put("image_url", entry.getImageUrl() != null ?entry.getImageUrl() :JSONObject.NULL); + obj.put("type", entry.getType()); + obj.put("last_message_time", entry.getLastMessageTime() != null ? entry.getLastMessageTime().toString() : JSONObject.NULL); + + jsonArray.put(obj); + } + + return jsonArray; + } + + +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/LoginBootstrapData.java b/src/main/java/org/to/telegramfinalproject/Models/LoginBootstrapData.java new file mode 100644 index 0000000..c15791f --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/LoginBootstrapData.java @@ -0,0 +1,32 @@ +package org.to.telegramfinalproject.Models; + +import java.util.List; + +public class LoginBootstrapData { + private User user; + private List contacts; + private List groups; + private List channels; + private List unreadMessages; + + public LoginBootstrapData(User user, List contacts, List groups, + List channels, List unreadMessages) { + this.user = user; + this.contacts = contacts; + this.groups = groups; + this.channels = channels; + this.unreadMessages = unreadMessages; + } + + public void setUnreadMessages(List unreadMessages){this.unreadMessages = unreadMessages;} + public void setContacts(List contacts){this.contacts =contacts;} + public void setGroups(List groups){this.groups = groups;} + public void setChannels(List channels){this.channels = channels;} + public void setUser(User user){this.user = user;} + + public List getUnreadMessages(){return unreadMessages;} + public List getGroups(){return groups;} + public List getContacts(){return contacts;} + public List getChannels(){return channels;} + public User getUser(User user){return user;} +} diff --git a/src/main/java/org/to/telegramfinalproject/Models/ResponseModel.java b/src/main/java/org/to/telegramfinalproject/Models/ResponseModel.java index 9d6ab0f..7434561 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ResponseModel.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ResponseModel.java @@ -1,12 +1,23 @@ package org.to.telegramfinalproject.Models; +import org.json.JSONObject; + public class ResponseModel { private String status; private String message; + private JSONObject data; + public ResponseModel(String status, String message) { this.status = status; this.message = message; + this.data = null; + } + + public ResponseModel(String status, String message, JSONObject data) { + this.status = status; + this.message = message; + this.data = data; } public String getStatus() { @@ -16,4 +27,6 @@ public class ResponseModel { public String getMessage() { return this.message; } + + public JSONObject getData() {return this.data;} } diff --git a/src/main/java/org/to/telegramfinalproject/Models/User.java b/src/main/java/org/to/telegramfinalproject/Models/User.java index 26e333d..9e5781e 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/User.java +++ b/src/main/java/org/to/telegramfinalproject/Models/User.java @@ -1,5 +1,7 @@ package org.to.telegramfinalproject.Models; +import org.json.JSONObject; + import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -17,6 +19,8 @@ public class User { private List contactList; private List groupList; private List channelList; + private List unreadMessages; + private List chatList; public User(String user_id, UUID internal_uuid, String username, String password, String profile_name) { this.user_id = user_id; @@ -64,6 +68,10 @@ public class User { public void setGroupList(List groupList){this.groupList = groupList;} + public void setUnreadMessages(List unreadMessages){this.unreadMessages = unreadMessages;} + + public void setChatList(List chatList){this.chatList = chatList;} + public UUID getInternal_uuid() { return this.internal_uuid; } @@ -105,5 +113,13 @@ public class User { public List getChannelList(){return this.channelList;} public List getGroupList(){return this.groupList;} + + public List getUnreadMessages(){return this.unreadMessages;} + + public List getChatList(){return this.chatList;} + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/AuthService.java b/src/main/java/org/to/telegramfinalproject/Server/AuthService.java index 587361b..1534c46 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/AuthService.java +++ b/src/main/java/org/to/telegramfinalproject/Server/AuthService.java @@ -41,6 +41,19 @@ public class AuthService { return user; } } + + public boolean loginCheck(String username , String password){ + User user = userDb.findByUsername(username); + String pass = user.getPassword(); + password = PasswordHashing.hash(password); + String Username = user.getUsername(); + boolean login = false; + if(user!= null && Username.equals(username)&& pass.equals(password)){ + login = true; + } + return login; + + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 6feb754..cda3323 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -1,12 +1,16 @@ package org.to.telegramfinalproject.Server; +import org.json.JSONArray; import org.json.JSONObject; -import org.to.telegramfinalproject.Models.RequestModel; -import org.to.telegramfinalproject.Models.ResponseModel; -import org.to.telegramfinalproject.Models.User; +import org.to.telegramfinalproject.Database.*; +import org.to.telegramfinalproject.Models.*; import java.io.*; import java.net.Socket; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; public class ClientHandler implements Runnable { private final Socket socket; @@ -34,9 +38,11 @@ public class ClientHandler implements Runnable { requestJson.optString("profile_name") ); - ResponseModel response; + ResponseModel response = null; - switch (request.getAction()) { + String action = requestJson.optString("action"); + + switch (action) { case "register": boolean registered = authService.register( request.getUser_id(), @@ -51,27 +57,159 @@ public class ClientHandler implements Runnable { case "login": User user = authService.login(request.getUsername(), request.getPassword()); - response = (user != null) - ? new ResponseModel("success", "Welcome " + user.getProfile_name()) - : new ResponseModel("error", "Login failed."); + if (user == null) { + response = new ResponseModel("error", "Login failed."); + } + else { + + SessionManager.addUser(user.getInternal_uuid(), this.socket); + userDatabase.updateUserStatus(user.getInternal_uuid(), "online"); + + + + List contacts = ContactDatabase.getContacts(user.getInternal_uuid()); + List groups = GroupDatabase.getGroupsByUser(user.getInternal_uuid()); + List channels = ChannelDatabase.getChannelsByUser(user.getInternal_uuid()); + List unreadMessages = MessageDatabase.getUnreadMessages(user.getInternal_uuid()); + + user.setContactList(contacts); + user.setGroupList(groups); + user.setChannelList(channels); + user.setUnreadMessages(unreadMessages); + + + List chatList = 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.getUser_id(), + target.getProfile_name(), + target.getImage_url(), + "private", + last + )); + } + + for (Group group : groups) { + LocalDateTime last = MessageDatabase.getLastMessageTime(group.getInternal_uuid(), "group"); + chatList.add(new ChatEntry( + group.getGroup_id(), + group.getGroup_name(), + group.getImage_url(), + "group", + last + )); + } + + for (Channel channel : channels) { + LocalDateTime last = MessageDatabase.getLastMessageTime(channel.getInternal_uuid(), "channel"); + chatList.add(new ChatEntry( + channel.getChannel_id(), + channel.getChannel_name(), + channel.getImage_url(), + "channel", + last + )); + } + + chatList.sort((a, b) -> { + if (a.getLastMessageTime() == null) return 1; + if (b.getLastMessageTime() == null) return -1; + return b.getLastMessageTime().compareTo(a.getLastMessageTime()); + }); + + + + JSONObject userData = JsonUtil.userToJson(user); + JSONArray chatListJson = JsonUtil.chatListToJson(chatList); + userData.put("chat_list", chatListJson); + + + response = new ResponseModel("success", "Welcome " + user.getProfile_name(), userData); + } + break; - case "logout": - response = new ResponseModel("success", "Logged out."); + case "logout": + + UUID uuid = UUID.fromString(request.getUser_id()); + userDatabase.updateUserStatus(uuid, "offline"); + userDatabase.updateLastSeen(uuid); + SessionManager.removeUser(uuid); + + response = new ResponseModel("success", "Logged out."); + break; + + case "search": + String keyword = requestJson.optString("keyword"); + + List results = new ArrayList<>(); + + // User search + List users = new userDatabase().searchUsers(keyword); + for (User u : users) { + JSONObject obj = new JSONObject(); + obj.put("type", "user"); + obj.put("id", u.getUser_id()); + obj.put("name", u.getProfile_name()); + results.add(obj); + } + + // Group search + List groups = GroupDatabase.searchGroups(keyword); + for (Group g : groups) { + JSONObject obj = new JSONObject(); + obj.put("type", "group"); + obj.put("id", g.getGroup_id()); + obj.put("name", g.getGroup_name()); + results.add(obj); + } + + // Channel search + List channels = ChannelDatabase.searchChannels(keyword); + for (Channel c : channels) { + JSONObject obj = new JSONObject(); + obj.put("type", "channel"); + obj.put("id", c.getChannel_id()); + obj.put("name", c.getChannel_name()); + results.add(obj); + } + + JSONArray dataArray = new JSONArray(results); + JSONObject data = new JSONObject(); + data.put("results", dataArray); + response = new ResponseModel("success", "Search results found", data); break; default: - response = new ResponseModel("error", "Unknown action: " + request.getAction()); + response = new ResponseModel("error", "Unknown action: " + action); + } + + JSONObject responseJson = new JSONObject(); + responseJson.put("status", response.getStatus()); + responseJson.put("message", response.getMessage()); + responseJson.put("data",response.getData() != null ?response.getData() :JSONObject.NULL); + out.println(responseJson.toString()); } - JSONObject responseJson = new JSONObject(); - responseJson.put("status", response.getStatus()); - responseJson.put("message", response.getMessage()); - out.println(responseJson.toString()); - } + } catch(IOException e){ + System.out.println("Connection with client lost."); - } catch (IOException e) { - System.out.println("Connection with client lost."); + UUID userId = SessionManager.getUserIdBySocket(this.socket); + if (userId != null) { + userDatabase.updateUserStatus(userId, "offline"); + userDatabase.updateLastSeen(userId); + SessionManager.removeUser(userId); + } + } } - } + + + } + diff --git a/src/main/java/org/to/telegramfinalproject/Server/SessionManager.java b/src/main/java/org/to/telegramfinalproject/Server/SessionManager.java new file mode 100644 index 0000000..351be7c --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/SessionManager.java @@ -0,0 +1,35 @@ +package org.to.telegramfinalproject.Server; + +import java.net.Socket; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionManager { + private static final Map onlineUsers = new ConcurrentHashMap<>(); //catch UUID for find the user socket + + public static void addUser(UUID userId, Socket socket) { + onlineUsers.put(userId, socket); + } + + public static void removeUser(UUID userId) { + onlineUsers.remove(userId); + } + + public static boolean isOnline(UUID userId) { + return onlineUsers.containsKey(userId); + } + + public static Socket getUserSocket(UUID userId) { + return onlineUsers.get(userId); + } + + public static UUID getUserIdBySocket(Socket socket) { + for (Map.Entry entry : onlineUsers.entrySet()) { + if (entry.getValue().equals(socket)) { + return entry.getKey(); + } + } + return null; + } +}