Merge remote-tracking branch 'origin/develop' into Sidebar-Menu

# Conflicts:
#	src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java
#	src/main/java/org/to/telegramfinalproject/Database/ContactDatabase.java
#	src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java
#	src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java
#	src/main/java/org/to/telegramfinalproject/Models/Message.java
#	src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java
#	src/main/resources/init.sql
This commit is contained in:
Asal Lotfi
2025-08-11 13:10:18 +03:30
16 changed files with 1939 additions and 220 deletions
@@ -344,6 +344,7 @@ public class ActionHandler {
}
public void createGroup() {
String groupId = null;
@@ -524,7 +525,7 @@ public class ActionHandler {
chat.optBoolean("is_admin", false)
);
if (chat.has("other_user_id")) {
entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat
entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id")));
}
archivedChats.add(entry);
@@ -545,7 +546,7 @@ public class ActionHandler {
chat.optBoolean("is_admin", false)
);
if (chat.has("other_user_id")) {
entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id"))); // 👈 اضافه کردن برای private chat
entry.setOtherUserId(UUID.fromString(chat.getString("other_user_id")));
}
activeChats.add(entry);
@@ -615,12 +616,24 @@ public class ActionHandler {
String type = item.getString("type");
if (type.equals("message")) {
System.out.println((i + 1) + ". [message] \"" + item.getString("content") + "\""
+ " (from: " + item.optString("sender", "N/A") + ", at: " + item.getString("time") + ")");
String senderName = item.optString("sender_name", item.optString("sender", "Unknown"));
String time = item.optString("time", "Unknown time");
String content = item.optString("content", "[No content]");
String context = "";
if (item.has("group_name")) {
context = " in group: " + item.getString("group_name");
} else if (item.has("channel_name")) {
context = " in channel: " + item.getString("channel_name");
}
System.out.println((i + 1) + ". [message] \"" + content + "\""
+ " (from: " + senderName + context + ", at: " + time + ")");
} else {
System.out.println((i + 1) + ". [" + type + "] "
+ item.getString("name") + " (ID: " + item.getString("id") + ")");
}
}
System.out.print("Select a result number to interact (or 0 to exit): ");
@@ -722,6 +735,22 @@ public class ActionHandler {
case "message" -> {
String receiverId = selected.getString("receiver_id");
String receiverType = selected.getString("receiver_type");
String senderName = selected.optString("sender_name", "Unknown");
String content = selected.optString("content", "[No content]");
String time = selected.optString("time", "Unknown time");
String context = "";
if (receiverType.equals("group")) {
context = "in group: " + selected.optString("group_name", "Unknown group");
} else if (receiverType.equals("channel")) {
context = "in channel: " + selected.optString("channel_name", "Unknown channel");
}
System.out.println("📩 Message preview:");
System.out.println("From: " + senderName);
if (!context.isEmpty()) System.out.println(context);
System.out.println("Time: " + time);
System.out.println("Content: \"" + content + "\"");
System.out.println("🔍 Looking for chat with displayId='" + receiverId + "' type='" + receiverType + "'");
@@ -733,6 +762,7 @@ public class ActionHandler {
openChat(chat);
}
default -> System.out.println("No interaction available for type: " + type);
}
}
@@ -1392,7 +1422,8 @@ public class ActionHandler {
System.out.println("4. Delete chat (both sides)");
System.out.println("5. View profile");
System.out.println("6. Archive/Unarchived");
System.out.println("7. Back");
System.out.println("7. View messages");
System.out.println("8. Back");
String input = scanner.nextLine();
@@ -1438,8 +1469,11 @@ public class ActionHandler {
return true;
}
case "7" ->{
viewMessagesInChat(chat);
}
case "7" -> {
case "8" -> {
return false;
}
default -> System.out.println("Invalid choice.");
@@ -1505,6 +1539,7 @@ public class ActionHandler {
}
System.out.println("12. Archive/Unarchived");
System.out.println("13. View messages");
System.out.println("0. Back to Chat List");
@@ -1559,6 +1594,10 @@ public class ActionHandler {
toggleArchive(chat.getId() , "group");
}
case "13" ->{
viewMessagesInChat(chat);
}
case "0" -> {
return false;
}
@@ -1626,6 +1665,7 @@ public class ActionHandler {
}
System.out.println("12. Archive/Unarchived");
System.out.println("13. View messages");
System.out.println("0. Back to Chat List");
String input = scanner.nextLine();
@@ -1708,7 +1748,9 @@ public class ActionHandler {
case "12"->{
toggleArchive(chat.getId() , "channel");
}
case "13" ->{
viewMessagesInChat(chat);
}
case "0" -> {
return false;
@@ -3169,7 +3211,7 @@ public class ActionHandler {
}
case "2" -> {
addContact(chat.getId());
refreshChatList();
refreshContactList();
}
default -> System.out.println("Back...");
}
@@ -3446,6 +3488,397 @@ public class ActionHandler {
}
}
private void refreshContactList() {
JSONObject req = new JSONObject();
req.put("action", "get_contact_list");
req.put("user_id", Session.currentUser.getString("user_id"));
out.println(req.toString());
try {
JSONObject response = TelegramClient.responseQueue.take();
if (response != null && response.getString("status").equals("success")) {
if (response.has("data") && !response.isNull("data")) {
JSONObject data = response.getJSONObject("data");
if (!data.has("contact_list") || data.isNull("contact_list")) {
System.out.println("❌ contact_list not found in response data.");
return;
}
JSONArray contactListJson = data.getJSONArray("contact_list");
List<ContactEntry> contactList = new ArrayList<>();
for (Object obj : contactListJson) {
JSONObject c = (JSONObject) obj;
ContactEntry entry = new ContactEntry(
UUID.fromString(c.getString("contact_id")),
c.getString("user_id"),
c.getString("profile_name"),
c.optString("image_url", ""),
c.optBoolean("is_blocked", false)
);
contactList.add(entry);
}
Session.contactEntries = contactList;
System.out.println("✅ Contact list updated. Total: " + contactList.size());
} else {
System.out.println("⚠️ Response has no data object.");
}
} else {
if (response.has("message") && !response.isNull("message")) {
System.out.println("❌ Failed to refresh contact list: " + response.getString("message"));
} else {
System.out.println("❌ Failed to refresh contact list.");
}
}
} catch (Exception e) {
System.err.println("❌ Error during refreshContactList: " + e.getMessage());
e.printStackTrace();
}
}
private void viewMessagesInChat(ChatEntry chat) {
int offset = 0;
int limit = 10;
while (true) {
JSONObject req = new JSONObject();
req.put("action", "get_chat_messages");
req.put("chat_id", chat.getId());
req.put("chat_type", chat.getType());
req.put("offset", offset);
req.put("limit", limit);
JSONObject res = sendWithResponse(req);
if (res == null || !res.getString("status").equals("success")) {
System.out.println("❌ Failed to fetch messages.");
return;
}
JSONArray messages = res.getJSONObject("data").getJSONArray("get_chat_messages");
if (messages.isEmpty()) {
if (offset == 0)
System.out.println("📭 No messages in this chat.");
else
System.out.println("📭 No more messages.");
return;
}
System.out.println("\n📥 Messages:");
System.out.println("─────────────────────────────────────────────");
for (int i = 0; i < messages.length(); i++) {
JSONObject msg = messages.getJSONObject(i);
String senderName = msg.optString("sender_name", "Unknown");
String content = msg.optString("content", "(no content)");
String time = msg.optString("time", "");
boolean isEdited = msg.optBoolean("is_edited", false);
String editedAt = msg.optString("edited_at", "");
String label = isEdited ? "🖊️ (edited at: " + editedAt + ")" : "";
// ✅ Forwarded info
String forwardLabel = "";
if (msg.optBoolean("is_forwarded", false)) {
String forwardFrom = msg.optString("forwarded_from_name", "Unknown");
forwardLabel = "🔁 Forwarded from " + forwardFrom;
}
//for reply
String replyLabel = "";
if (msg.has("reply_to_id")) {
String repliedSender = msg.optString("reply_to_sender", "Unknown");
String repliedContent = msg.optString("reply_to_content", "...");
replyLabel = "↪️ Reply to " + repliedSender + ": \"" + repliedContent + "\"";
}
JSONArray reactions = msg.optJSONArray("reactions");
if (reactions != null && !reactions.isEmpty()) {
System.out.print(" 💬 Reactions: ");
for (int j = 0; j < reactions.length(); j++) {
System.out.print(reactions.getString(j) + " ");
}
System.out.println();
}
System.out.printf("[%d] [%s] %s: \n", i + 1, time, senderName);
if (!forwardLabel.isEmpty()) System.out.println(forwardLabel);
if (!replyLabel.isEmpty()) System.out.println(replyLabel);
System.out.printf("%s %s\n", content, label);
System.out.println("─────────────────────────────────────────────");
}
System.out.print("""
💬 Options:
[number] - Interact with message
N - Next page (older messages)
0 - Back to chat menu
S - Send message
➤ Choice: """);
String input = scanner.nextLine().trim();
if (input.equals("0")) return;
if (input.equalsIgnoreCase("N")) {
offset += limit;
continue;
}
if(input.equalsIgnoreCase("S")){
sendMessage(chat.getId(), chat.getType());
}
try {
int index = Integer.parseInt(input);
if (index < 1 || index > messages.length()) {
System.out.println("❌ Invalid message number.");
continue;
}
JSONObject selected = messages.getJSONObject(index - 1);
UUID messageId = UUID.fromString(selected.getString("message_id"));
UUID senderId = UUID.fromString(selected.getString("sender_id"));
boolean isSender = senderId.toString().equals(Session.currentUser.getString("internal_uuid"));
boolean isChannel = chat.getType().equals("channel");
boolean isOwnerOrAdmin = chat.isOwner() || chat.isAdmin();
System.out.println("\n🎯 Selected message by " + selected.getString("sender_name"));
// ✔ Show interaction menu based on access
System.out.println("2. Forward");
if (isChannel) {
if (isOwnerOrAdmin) {
System.out.println("1. Reply");
System.out.println("3. React");
if (isSender) {
System.out.println("4. Edit");
}
System.out.println("5. Delete");
}
} else {
System.out.println("1. Reply");
System.out.println("3. React");
if (isSender) {
System.out.println("4. Edit");
}
if (isSender || isOwnerOrAdmin) {
System.out.println("5. Delete");
}
}
System.out.println("0. Back to message list");
System.out.print("➤ Select an action: ");
String choice = scanner.nextLine().trim();
switch (choice) {
case "1" -> {
if (!isChannel || isOwnerOrAdmin)
replyToMessage(messageId);
else
System.out.println("❌ You are not allowed to reply.");
}
case "2" -> forwardMessage(messageId);
case "3" -> {
if (!isChannel || isOwnerOrAdmin)
reactToMessage(messageId);
else
System.out.println("❌ You are not allowed to react.");
}
case "4" -> {
if (isSender)
editMessage(messageId);
else
System.out.println("❌ You can only edit your own messages.");
}
case "5" -> {
if (!isChannel || isOwnerOrAdmin || isSender)
deleteMessage(messageId);
else
System.out.println("❌ You are not allowed to delete this message.");
}
case "0" -> {}
default -> System.out.println("❌ Invalid option.");
}
} catch (NumberFormatException e) {
System.out.println("❌ Please enter a valid number or command.");
}
}
}
private void editMessage(UUID messageId) {
System.out.print("📝 Enter new content: ");
String newContent = scanner.nextLine().trim();
if (newContent.isEmpty()) {
System.out.println("❌ Content cannot be empty.");
return;
}
JSONObject req = new JSONObject();
req.put("action", "edit_message");
req.put("message_id", messageId.toString());
req.put("new_content", newContent);
JSONObject res = sendWithResponse(req);
if (res == null || !res.getString("status").equals("success")) {
System.out.println("❌ Failed to edit message.");
return;
}
System.out.println("✅ Message updated successfully.");
}
private void reactToMessage(UUID messageId) {
System.out.print("😊 Enter your reaction (e.g., ❤️, 👍, 😂): ");
String reaction = scanner.nextLine().trim();
if (reaction.isEmpty()) {
System.out.println("⚠️ Empty reaction discarded.");
return;
}
JSONObject req = new JSONObject();
req.put("action", "react_to_message");
req.put("message_id", messageId.toString());
req.put("reaction", reaction);
JSONObject res = sendWithResponse(req);
if (res != null && res.getString("status").equals("success")) {
System.out.println("✅ Reaction sent.");
} else {
System.out.println("❌ Failed to react to message.");
}
}
private void forwardMessage(UUID originalMessageId) {
System.out.println("\n📤 Select a chat to forward this message to:");
List<ChatEntry> chatList = Session.chatList;
for (int i = 0; i < chatList.size(); i++) {
ChatEntry c = chatList.get(i);
System.out.printf("%d. [%s] %s\n", i + 1, c.getType(), c.getName());
}
System.out.println("0. Cancel");
System.out.print("➤ Enter chat number: ");
String input = scanner.nextLine().trim();
if (input.equals("0")) return;
try {
int choice = Integer.parseInt(input);
if (choice < 1 || choice > chatList.size()) {
System.out.println("❌ Invalid choice.");
return;
}
ChatEntry target = chatList.get(choice - 1);
JSONObject req = new JSONObject();
req.put("action", "forward_message");
req.put("target_chat_id", target.getId());
req.put("target_chat_type", target.getType());
req.put("original_message_id", originalMessageId.toString());
req.put("forwarded_by", Session.currentUser.getString("internal_uuid"));
JSONObject res = sendWithResponse(req);
if (res != null && res.getString("status").equals("success")) {
System.out.println("✅ Message forwarded successfully.");
} else {
System.out.println("❌ Failed to forward the message.");
}
} catch (NumberFormatException e) {
System.out.println("❌ Invalid input.");
}
}
private void replyToMessage(UUID messageId) {
System.out.print("💬 Enter your reply: ");
String content = scanner.nextLine().trim();
if (content.isEmpty()) {
System.out.println("⚠️ Empty reply discarded.");
return;
}
UUID chatId = UUID.fromString(Session.currentChatId);
String chatType = Session.currentChatType;
JSONObject req = new JSONObject();
req.put("action", "send_reply_message");
req.put("receiver_type", chatType);
req.put("receiver_id", chatId.toString());
req.put("content", content);
req.put("reply_to_id", messageId.toString());
JSONObject res = sendWithResponse(req);
if (res != null && res.getString("status").equals("success")) {
System.out.println("✅ Reply sent.");
} else {
System.out.println("❌ Failed to send reply.");
}
}
private void deleteMessage(UUID messageId) {
System.out.println("\n🗑️ Delete Message Options:");
System.out.println("1. Delete for yourself (one-sided)");
System.out.println("2. Delete for everyone (global) [only if allowed]");
System.out.println("0. Cancel");
String choice = scanner.nextLine().trim();
switch (choice) {
case "1" -> {
JSONObject req = new JSONObject();
req.put("action", "delete_message");
req.put("message_id", messageId.toString());
req.put("delete_type", "one-sided");
JSONObject res = sendWithResponse(req);
if (res != null && res.getString("status").equals("success")) {
System.out.println("✅ Message deleted for you.");
} else {
System.out.println("❌ Failed to delete message.");
}
}
case "2" -> {
JSONObject req = new JSONObject();
req.put("action", "delete_message");
req.put("message_id", messageId.toString());
req.put("delete_type", "global");
JSONObject res = sendWithResponse(req);
if (res != null && res.getString("status").equals("success")) {
System.out.println("✅ Message deleted for everyone.");
} else {
System.out.println("❌ Failed to delete message globally.");
if (res != null) System.out.println("⚠️ " + res.optString("message"));
}
}
case "0" -> {
System.out.println("❎ Delete canceled.");
}
default -> {
System.out.println("❌ Invalid option.");
}
}
}
private void showSidebarMenu() {
System.out.println("\n--- Sidebar Menu ---");
System.out.println("1. View Profile");
@@ -3493,6 +3926,7 @@ public class ActionHandler {
}
}
}
@@ -74,24 +74,24 @@ public class IncomingMessageListener implements Runnable {
private boolean isRealTimeEvent(String action) {
return switch (action) {
case "new_message", "message_edited", "message_deleted",
case "new_message", "message_edited", "message_deleted_global",
"user_status_changed", "added_to_group", "added_to_channel",
"update_group_or_channel", "chat_deleted",
"blocked_by_user", "unblocked_by_user", "message_seen",
"removed_from_group", "removed_from_channel",
"became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> true;
"became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted" -> true;
default -> false;
};
}
void handleRealTimeEvent(JSONObject response) throws IOException {
String action = response.getString("action");
JSONObject msg = response.getJSONObject("data");
JSONObject msg = response.has("data") ? response.getJSONObject("data") : new JSONObject();
switch (action) {
case "added_to_group", "added_to_channel",
"removed_from_group", "removed_from_channel", "chat_deleted" -> {
"removed_from_group", "removed_from_channel", "chat_deleted","created_private_chat" -> {
System.out.println("🔄 Chat list changed. Updating...");
Session.forceRefreshChatList = true;
System.out.println("🧪 Calling requestChatList() after being added");
@@ -135,10 +135,6 @@ public class IncomingMessageListener implements Runnable {
}
//
default -> displayRealTimeMessage(action, msg);
}
@@ -298,36 +294,52 @@ public class IncomingMessageListener implements Runnable {
private void displayRealTimeMessage(String action, JSONObject msg) {
switch (action) {
case "new_message" -> {
System.out.println("\n🔔 New Message Received:");
String senderName = msg.optString("sender_name", "Unknown");
String content = msg.optString("content", "(empty)");
String sendAt = msg.optString("send_at", "-");
String senderName = msg.optString("sender_name","Unknown");
String content = msg.optString("content","(empty)");
String sendAt = msg.optString("send_at","-");
String chatId = msg.optString("receiver_id", msg.optString("chat_id",""));
String kind = msg.optString("kind","plain");
JSONObject meta = msg.optJSONObject("meta");
String receiverId = msg.optString("receiver_id", "");
String receiverType = msg.optString("receiver_type", "");
String prefix = "";
if ("reply".equals(kind) && meta != null) {
var rt = meta.optJSONObject("reply_to");
prefix = "[reply → " + (rt!=null?rt.optString("excerpt",""):"") + "] ";
} else if ("forward".equals(kind) && meta != null) {
var ff = meta.optJSONObject("forwarded_from");
prefix = "[forwarded from " + (ff!=null?ff.optString("sender_name","unknown"):"unknown") + "] ";
}
boolean isInCurrentChat = Session.inChatMenu &&
Session.currentChatId != null &&
Session.currentChatId.equals(receiverId);
Session.currentChatId != null && Session.currentChatId.equals(chatId);
if (isInCurrentChat) {
System.out.println(senderName + ": " + content + " (" + sendAt + ")");
System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")");
} else {
System.out.println("💬 Message from " + senderName + " in " + receiverType + " chat: " + content);
System.out.println("💬 Message from " + senderName + ": " + prefix + content);
Session.forceRefreshChatList = true;
}
}
case "message_edited" -> {
System.out.println("\n✏️ Message Edited:");
System.out.println("ID: " + msg.getString("message_id"));
System.out.println("New Content: " + msg.getString("new_content"));
System.out.println("Edit Time: " + msg.getString("edited_at"));
}
case "message_deleted" -> {
case "message_deleted_global" -> {
System.out.println("\n🗑️ Message Deleted:");
System.out.println("Message ID: " + msg.getString("message_id"));
}
case "message_reacted", "message_unreacted" -> {
String mid = msg.getString("message_id");
String emoji = msg.getString("emoji");
JSONObject counts = msg.optJSONObject("counts");
int n = msg.optInt("count_for_emoji", 0);
System.out.println("\n⭐ Reaction update on " + mid + " : " + emoji + "" + n);
}
case "user_status_changed" -> {
System.out.println("\n🔄 User Status Changed:");
System.out.println("User: " + msg.getString("user_id"));
@@ -27,8 +27,7 @@ public class Session {
public static String currentChatId = null;
public static ChatEntry currentChatEntry = null;
public static List<ContactEntry> contactEntries = new ArrayList<>();
public static boolean inContactListMenu = false;
public static String getUserUUID() {
@@ -82,7 +82,6 @@ public class ContactDatabase {
}
return !currentlyBlocked;
} else {
// اگر رابطه وجود نداره، اول باید کاربر رو به contact ها اضافه کنیم
String insertSql = "INSERT INTO contacts (user_id, contact_id, is_blocked) VALUES (?, ?, ?)";
try (PreparedStatement insertStmt = conn.prepareStatement(insertSql)) {
insertStmt.setObject(1, userId);
@@ -152,8 +151,13 @@ public class ContactDatabase {
}
}
public boolean eitherBlocks(UUID userA, UUID userB) {
return isBlocked(userA, userB) || isBlocked(userB, userA);
}
public boolean isBlocked(UUID user_id, UUID contact_id) {
public static boolean isBlocked(UUID user_id, UUID contact_id) {
String sql = "SELECT is_blocked FROM contacts WHERE user_id = ? AND contact_id = ?";
try (Connection connection = getConnection()) {
PreparedStatement stmt = connection.prepareStatement(sql);
@@ -241,6 +245,39 @@ public class ContactDatabase {
}
}
public static List<ContactEntry> getContactEntries(UUID userId) {
List<ContactEntry> entries = new ArrayList<>();
String sql = """
SELECT c.contact_id, c.is_blocked, u.user_id, u.profile_name, u.image_url
FROM contacts c
JOIN users u ON c.contact_id = u.internal_uuid
WHERE c.user_id = ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, userId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
UUID contactId = UUID.fromString(rs.getString("contact_id"));
boolean isBlocked = rs.getBoolean("is_blocked");
String userIdStr = rs.getString("user_id");
String profileName = rs.getString("profile_name");
String imageUrl = rs.getString("image_url");
ContactEntry entry = new ContactEntry(contactId, userIdStr, profileName, imageUrl, isBlocked);
entries.add(entry);
}
} catch (SQLException e) {
e.printStackTrace();
}
return entries;
}
public static List<ContactEntry> searchContacts(UUID userId, String searchTerm) {
List<ContactEntry> results = new ArrayList<>();
@@ -6,6 +6,7 @@ import org.to.telegramfinalproject.Models.Message;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -76,7 +77,7 @@ public class MessageDatabase {
}
public static void logDeletedMessagesFor(UUID chatId, UUID userId) {
List<Message> messages = MessageDatabase.privateChatHistory(chatId);
List<Message> messages = MessageDatabase.privateChatHistory(chatId, userId);
for (Message message : messages) {
if (!isMessageDeleted(message.getMessage_id(), userId)) {
String sql = """
@@ -110,6 +111,258 @@ public class MessageDatabase {
}
}
public static List<Message> getMessagesForChat(UUID chatId, String chatType, UUID currentUserId, int offset, int limit) {
List<Message> messages = new ArrayList<>();
String sql = """
SELECT * FROM messages m
WHERE m.receiver_type = ?
AND m.receiver_id = ?
AND m.is_deleted_globally = FALSE
AND NOT EXISTS (
SELECT 1 FROM deleted_messages d
WHERE d.message_id = m.message_id
AND d.user_id = ?
)
ORDER BY m.send_at DESC
LIMIT ? OFFSET ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, chatType);
ps.setObject(2, chatId);
ps.setObject(3, currentUserId);
ps.setInt(4, limit);
ps.setInt(5, offset);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Message msg = new Message(
UUID.fromString(rs.getString("message_id")),
UUID.fromString(rs.getString("sender_id")),
rs.getString("receiver_type"),
UUID.fromString(rs.getString("receiver_id")),
rs.getString("content"),
rs.getString("message_type"),
rs.getTimestamp("send_at").toLocalDateTime(),
rs.getString("status"),
rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null,
rs.getBoolean("is_edited"),
rs.getBoolean("is_deleted_globally"),
rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null,
rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null,
rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null,
rs.getTimestamp("edited_at") != null
? rs.getTimestamp("edited_at").toLocalDateTime()
: null
);
messages.add(msg);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return messages;
}
public static Message findById(UUID messageId) {
String sql = "SELECT * FROM messages WHERE message_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return new Message(
UUID.fromString(rs.getString("message_id")),
UUID.fromString(rs.getString("sender_id")),
rs.getString("receiver_type"),
UUID.fromString(rs.getString("receiver_id")),
rs.getString("content"),
rs.getString("message_type"),
rs.getTimestamp("send_at").toLocalDateTime(),
rs.getString("status"),
rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null,
rs.getBoolean("is_edited"),
rs.getBoolean("is_deleted_globally"),
rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null,
rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null,
rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null
);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static boolean markAsDeletedForUser(UUID messageId, UUID userId) {
String sql = "INSERT INTO deleted_messages (message_id, user_id) VALUES (?, ?) ON CONFLICT DO NOTHING";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
ps.setObject(2, userId);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean markAsGloballyDeleted(UUID messageId) {
String sql = "UPDATE messages SET is_deleted_globally = true WHERE message_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean updateContentAndMarkEdited(UUID messageId, String newContent) {
String sql = """
UPDATE messages
SET content = ?, is_edited = TRUE, edited_at = CURRENT_TIMESTAMP
WHERE message_id = ?
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, newContent);
ps.setObject(2, messageId);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean saveReplyMessage(Message message) {
String sql = """
INSERT INTO messages (
message_id, sender_id, receiver_type, receiver_id, content, message_type,
send_at, status, reply_to_id, is_edited, is_deleted_globally,
original_message_id, forwarded_by, forwarded_from, edited_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, message.getMessage_id());
ps.setObject(2, message.getSender_id());
ps.setString(3, message.getReceiver_type());
ps.setObject(4, message.getReceiver_id());
ps.setString(5, message.getContent());
ps.setString(6, message.getMessage_type());
ps.setTimestamp(7, Timestamp.valueOf(message.getSend_at()));
ps.setString(8, message.getStatus());
ps.setObject(9, message.getReply_to_id()); //Important part because of reply message id
ps.setBoolean(10, message.isIs_edited());
ps.setBoolean(11, message.isIs_deleted_globally());
ps.setObject(12, message.getOriginal_message_id());
ps.setObject(13, message.getForwarded_by());
ps.setObject(14, message.getForwarded_from());
ps.setTimestamp(15, message.getEdited_at() != null ? Timestamp.valueOf(message.getEdited_at()) : null);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static boolean saveForwardedMessage(Message message) {
String sql = """
INSERT INTO messages (
message_id, sender_id, receiver_type, receiver_id,
content, message_type, send_at, status,
reply_to_id, is_edited, is_deleted_globally,
original_message_id, forwarded_by, forwarded_from
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, message.getMessage_id());
ps.setObject(2, message.getSender_id());
ps.setString(3, message.getReceiver_type());
ps.setObject(4, message.getReceiver_id());
ps.setString(5, message.getContent());
ps.setString(6, message.getMessage_type());
ps.setTimestamp(7, Timestamp.valueOf(message.getSend_at()));
ps.setString(8, message.getStatus());
ps.setObject(9, message.getReply_to_id());
ps.setBoolean(10, message.isIs_edited());
ps.setBoolean(11, message.isIs_deleted_globally());
ps.setObject(12, message.getOriginal_message_id()); //original id
ps.setObject(13, message.getForwarded_by()); //forwarded by
ps.setObject(14, message.getForwarded_from()); //forwarded from
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static String getExcerpt(UUID messageId) {
return getExcerpt(messageId, 80);
}
public static String getExcerpt(UUID messageId, int maxLen) {
String sql = "SELECT content, message_type, is_deleted_globally FROM messages WHERE message_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return "Message isn't available";
String content = rs.getString("content");
String type = rs.getString("message_type"); // e.g. TEXT, IMAGE, VIDEO, AUDIO, FILE, STICKER...
boolean deleted = rs.getBoolean("is_deleted_globally");
if (deleted) return "This message was deleted";
if (type == null || type.equalsIgnoreCase("TEXT")) {
if (content == null || content.isBlank()) return "(empty)";
return shorten(content, maxLen);
}
//for media
switch (type.toUpperCase()) {
case "IMAGE": return "[Photo]";
case "VIDEO": return "[Video]";
case "AUDIO": return "[Audio]";
case "VOICE": return "[Voice]";
case "FILE": return "[File]";
case "STICKER":return "[Sticker]";
default: return "[" + type + "]";
}
}
} catch (SQLException e) {
e.printStackTrace();
return "Error";
}
}
private static String shorten(String s, int maxLen) {
if (s.length() <= maxLen) return s;
return s.substring(0, Math.max(0, maxLen - 1)).trim() + "";
}
@@ -128,38 +381,43 @@ public class MessageDatabase {
public static List<Message> getUnreadMessages(UUID userId) {
List<Message> messages = new ArrayList<>();
String sql = """
SELECT m.* FROM messages m
SELECT m.*
FROM messages m
LEFT JOIN message_receipts r ON m.message_id = r.message_id AND r.user_id = ?
LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ?
WHERE r.user_id IS NULL
AND (
(m.receiver_type = 'private' AND m.receiver_id = ?)
OR
(m.receiver_type = 'group' AND EXISTS (
SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ?
))
OR
(m.receiver_type = 'channel' AND EXISTS (
SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ?
))
)
AND d.message_id IS NULL
AND m.is_deleted_globally = FALSE
AND (
(m.receiver_type = 'private' AND m.receiver_id = ?)
OR
(m.receiver_type = 'group' AND EXISTS (
SELECT 1 FROM group_members gm WHERE gm.group_id = m.receiver_id AND gm.user_id = ?
))
OR
(m.receiver_type = 'channel' AND EXISTS (
SELECT 1 FROM channel_subscribers cs WHERE cs.channel_id = m.receiver_id AND cs.user_id = ?
))
)
ORDER BY m.send_at DESC
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setObject(2, userId);
stmt.setObject(3, userId);
stmt.setObject(4, userId);
stmt.setObject(1, userId); // for message_receipts
stmt.setObject(2, userId); // for deleted_messages
stmt.setObject(3, userId); // for private messages
stmt.setObject(4, userId); // for group members
stmt.setObject(5, userId); // for channel subscribers
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
messages.add(new Message(
Message message = new Message(
UUID.fromString(rs.getString("message_id")),
rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null,
rs.getString("receiver_type"),
@@ -170,12 +428,15 @@ public class MessageDatabase {
rs.getString("status"),
rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null,
rs.getBoolean("is_edited"),
rs.getBoolean("is_deleted_globally"),
rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null,
rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null,
rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null,
rs.getBoolean("is_deleted_globally"),
rs.getTimestamp("edited_at") != null ? rs.getTimestamp("edited_at").toLocalDateTime() : null
));
messages.add(message);
}
} catch (SQLException e) {
@@ -185,6 +446,8 @@ public class MessageDatabase {
return messages;
}
public static List<FileAttachment> getAttachments(UUID messageId) {
List<FileAttachment> attachments = new ArrayList<>();
String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?";
@@ -283,45 +546,129 @@ public class MessageDatabase {
// public static List<Message> searchMessagesForUser(UUID userId, String keyword) {
// List<Message> result = new ArrayList<>();
// String sql = """
// SELECT * FROM messages
// WHERE receiver_type = 'private'
// AND (sender_id = ? OR receiver_id = ?)
// AND content ILIKE ?
// ORDER BY send_at DESC
// """;
//
// try (Connection conn = ConnectionDb.connect();
// PreparedStatement stmt = conn.prepareStatement(sql)) {
// stmt.setObject(1, userId);
// stmt.setObject(2, userId);
// stmt.setString(3, "%" + keyword + "%");
//
// ResultSet rs = stmt.executeQuery();
// while (rs.next()) {
// result.add(extractMessage(rs));
// }
// } catch (SQLException e) {
// e.printStackTrace();
// }
//
// return result;
// }
public static List<Message> searchMessagesForUser(UUID userId, String keyword) {
List<Message> result = new ArrayList<>();
String sql = """
SELECT * FROM messages
WHERE receiver_type = 'private'
AND (sender_id = ? OR receiver_id = ?)
AND content ILIKE ?
ORDER BY send_at DESC
""";
SELECT * FROM messages m
WHERE m.receiver_type = 'private'
AND m.content ILIKE ?
AND m.is_deleted_globally = FALSE
AND (m.sender_id = ? OR m.receiver_id = ?)
AND NOT EXISTS (
SELECT 1 FROM deleted_messages d
WHERE d.message_id = m.message_id AND d.user_id = ?
)
ORDER BY m.send_at DESC
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, userId);
stmt.setObject(2, userId);
stmt.setString(3, "%" + keyword + "%");
PreparedStatement ps = conn.prepareStatement(sql)) {
ResultSet rs = stmt.executeQuery();
ps.setString(1, "%" + keyword + "%");
ps.setObject(2, userId); // m.sender_id
ps.setObject(3, userId); // m.receiver_id
ps.setObject(4, userId); // deleted_messages
ResultSet rs = ps.executeQuery();
List<Message> messages = new ArrayList<>();
while (rs.next()) {
result.add(extractMessage(rs));
messages.add(mapResultSetToMessage(rs));
}
return messages;
} catch (SQLException e) {
e.printStackTrace();
return Collections.emptyList();
}
return result;
}
public static List<Message> privateChatHistory(UUID chatId) {
private static Message mapResultSetToMessage(ResultSet rs) throws SQLException {
return new Message(
UUID.fromString(rs.getString("message_id")),
rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null,
rs.getString("receiver_type"),
UUID.fromString(rs.getString("receiver_id")),
rs.getString("content"),
rs.getString("message_type"),
rs.getTimestamp("send_at").toLocalDateTime(),
rs.getString("status"),
rs.getObject("reply_to_id") != null ? UUID.fromString(rs.getString("reply_to_id")) : null,
rs.getBoolean("is_edited"),
rs.getBoolean("is_deleted_globally"),
rs.getObject("original_message_id") != null ? UUID.fromString(rs.getString("original_message_id")) : null,
rs.getObject("forwarded_by") != null ? UUID.fromString(rs.getString("forwarded_by")) : null,
rs.getObject("forwarded_from") != null ? UUID.fromString(rs.getString("forwarded_from")) : null
);
}
// public static List<Message> privateChatHistory(UUID chatId) {
// List<Message> result = new ArrayList<>();
// String sql = """
// SELECT * FROM messages
// WHERE receiver_type = 'private'
// AND receiver_id = ?
// ORDER BY send_at
// """;
//
// try (Connection conn = ConnectionDb.connect();
// PreparedStatement stmt = conn.prepareStatement(sql)) {
// stmt.setObject(1, chatId);
// ResultSet rs = stmt.executeQuery();
// while (rs.next()) {
// result.add(extractMessage(rs));
// }
// } catch (SQLException e) {
// e.printStackTrace();
// }
// return result;
// }
public static List<Message> privateChatHistory(UUID chatId, UUID userId) {
List<Message> result = new ArrayList<>();
String sql = """
SELECT * FROM messages
WHERE receiver_type = 'private'
AND receiver_id = ?
AND receiver_id = ?
AND is_deleted_globally = FALSE
AND message_id NOT IN (
SELECT message_id FROM deleted_messages WHERE user_id = ?
)
ORDER BY send_at
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, chatId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.add(extractMessage(rs));
@@ -335,17 +682,47 @@ public class MessageDatabase {
public static List<Message> groupChatHistory(UUID groupId) {
// public static List<Message> groupChatHistory(UUID groupId) {
// List<Message> result = new ArrayList<>();
// String sql = """
// SELECT * FROM messages
// WHERE receiver_type = 'group' AND receiver_id = ?
// ORDER BY send_at
// """;
//
// try (Connection conn = ConnectionDb.connect();
// PreparedStatement stmt = conn.prepareStatement(sql)) {
// stmt.setObject(1, groupId);
// ResultSet rs = stmt.executeQuery();
// while (rs.next()) {
// result.add(extractMessage(rs));
// }
// } catch (SQLException e) {
// e.printStackTrace();
// }
// return result;
// }
public static List<Message> groupChatHistory(UUID groupId,UUID userId) {
List<Message> result = new ArrayList<>();
String sql = """
SELECT * FROM messages
WHERE receiver_type = 'group' AND receiver_id = ?
WHERE receiver_type = 'group'
AND receiver_id = ?
AND is_deleted_globally = FALSE
AND message_id NOT IN (
SELECT message_id FROM deleted_messages WHERE user_id = ?
)
ORDER BY send_at
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, groupId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.add(extractMessage(rs));
@@ -357,17 +734,48 @@ public class MessageDatabase {
}
public static List<Message> channelChatHistory(UUID channelId) {
// public static List<Message> channelChatHistory(UUID channelId) {
// List<Message> result = new ArrayList<>();
// String sql = """
// SELECT * FROM messages
// WHERE receiver_type = 'channel' AND receiver_id = ?
// ORDER BY send_at
// """;
//
// try (Connection conn = ConnectionDb.connect();
// PreparedStatement stmt = conn.prepareStatement(sql)) {
// stmt.setObject(1, channelId);
// ResultSet rs = stmt.executeQuery();
// while (rs.next()) {
// result.add(extractMessage(rs));
// }
// } catch (SQLException e) {
// e.printStackTrace();
// }
// return result;
// }
public static List<Message> channelChatHistory(UUID channelId, UUID userId) {
List<Message> result = new ArrayList<>();
String sql = """
SELECT * FROM messages
WHERE receiver_type = 'channel' AND receiver_id = ?
WHERE receiver_type = 'channel'
AND receiver_id = ?
AND is_deleted_globally = FALSE
AND message_id NOT IN (
SELECT message_id FROM deleted_messages WHERE user_id = ?
)
ORDER BY send_at
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, channelId);
stmt.setObject(2, userId);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.add(extractMessage(rs));
@@ -379,32 +787,75 @@ public class MessageDatabase {
}
public static List<Message> searchMessagesInGroups(List<UUID> groupIds, String keyword) {
// public static List<Message> searchMessagesInGroups(List<UUID> groupIds, String keyword) {
// List<Message> result = new ArrayList<>();
// if (groupIds.isEmpty()) return result;
//
// String placeholders = groupIds.stream().map(id -> "?").collect(Collectors.joining(", "));
// String sql = """
// SELECT * FROM messages
// WHERE receiver_type = 'group'
// AND receiver_id IN (%s)
// AND content ILIKE ?
// ORDER BY send_at DESC
// """.formatted(placeholders);
//
// try (Connection conn = ConnectionDb.connect();
// PreparedStatement stmt = conn.prepareStatement(sql)) {
//
// int i = 1;
// for (UUID id : groupIds) {
// stmt.setObject(i++, id);
// }
// stmt.setString(i, "%" + keyword + "%");
//
// ResultSet rs = stmt.executeQuery();
// while (rs.next()) {
// result.add(extractMessage(rs));
// }
// } catch (SQLException e) {
// e.printStackTrace();
// }
//
// return result;
// }
public static List<Message> searchMessagesInGroups(List<UUID> groupIds, String keyword, UUID userId) {
List<Message> result = new ArrayList<>();
if (groupIds.isEmpty()) return result;
String placeholders = groupIds.stream().map(id -> "?").collect(Collectors.joining(", "));
String sql = """
SELECT * FROM messages
WHERE receiver_type = 'group'
AND receiver_id IN (%s)
AND content ILIKE ?
ORDER BY send_at DESC
SELECT m.*
FROM messages m
LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ?
WHERE m.receiver_type = 'group'
AND m.receiver_id IN (%s)
AND d.message_id IS NULL
AND m.is_deleted_globally = FALSE
AND m.content ILIKE ?
ORDER BY m.send_at DESC
""".formatted(placeholders);
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
int i = 1;
for (UUID id : groupIds) {
stmt.setObject(i++, id);
stmt.setObject(i++, userId);
for (UUID groupId : groupIds) {
stmt.setObject(i++, groupId);
}
stmt.setString(i, "%" + keyword + "%");
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.add(extractMessage(rs));
result.add(mapResultSetToMessage(rs));
}
} catch (SQLException e) {
e.printStackTrace();
}
@@ -413,32 +864,71 @@ public class MessageDatabase {
}
// public static List<Message> searchMessagesInChannels(List<UUID> channelIds, String keyword) {
// List<Message> result = new ArrayList<>();
// if (channelIds.isEmpty()) return result;
//
// String placeholders = channelIds.stream().map(id -> "?").collect(Collectors.joining(", "));
// String sql = """
// SELECT * FROM messages
// WHERE receiver_type = 'channel'
// AND receiver_id IN (%s)
// AND content ILIKE ?
// ORDER BY send_at DESC
// """.formatted(placeholders);
//
// try (Connection conn = ConnectionDb.connect();
// PreparedStatement stmt = conn.prepareStatement(sql)) {
//
// int i = 1;
// for (UUID id : channelIds) {
// stmt.setObject(i++, id);
// }
// stmt.setString(i, "%" + keyword + "%");
//
// ResultSet rs = stmt.executeQuery();
// while (rs.next()) {
// result.add(extractMessage(rs));
// }
// } catch (SQLException e) {
// e.printStackTrace();
// }
//
// return result;
// }
public static List<Message> searchMessagesInChannels(List<UUID> channelIds, String keyword) {
List<Message> result = new ArrayList<>();
if (channelIds.isEmpty()) return result;
String placeholders = channelIds.stream().map(id -> "?").collect(Collectors.joining(", "));
String sql = """
SELECT * FROM messages
WHERE receiver_type = 'channel'
AND receiver_id IN (%s)
AND content ILIKE ?
ORDER BY send_at DESC
SELECT m.*
FROM messages m
WHERE m.receiver_type = 'channel'
AND m.receiver_id IN (%s)
AND m.is_deleted_globally = FALSE
AND m.content ILIKE ?
ORDER BY m.send_at DESC
""".formatted(placeholders);
try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) {
int i = 1;
for (UUID id : channelIds) {
stmt.setObject(i++, id);
for (UUID channelId : channelIds) {
stmt.setObject(i++, channelId);
}
stmt.setString(i, "%" + keyword + "%");
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
result.add(extractMessage(rs));
result.add(mapResultSetToMessage(rs));
}
} catch (SQLException e) {
e.printStackTrace();
}
@@ -484,6 +974,37 @@ public class MessageDatabase {
return messages;
}
public static List<Message> getMessages(UUID chatId, UUID userId) {
String sql = """
SELECT * FROM messages
WHERE receiver_id = ?
AND is_deleted_globally = false
AND message_id NOT IN (
SELECT message_id FROM deleted_messages
WHERE user_id = ?
)
ORDER BY send_at
""";
List<Message> messages = new ArrayList<>();
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, chatId);
ps.setObject(2, userId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Message m = extractMessage(rs);
messages.add(m);
}
} catch (SQLException e) {
e.printStackTrace();
}
return messages;
}
public static boolean insertSavedMessage(Message message) {
String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type, send_at, status, reply_to_id, is_edited, edited_at," +
" original_message_id, forwarded_by, forwarded_from, is_deleted_globally) " +
@@ -537,5 +1058,4 @@ public class MessageDatabase {
}
}
}
@@ -0,0 +1,76 @@
package org.to.telegramfinalproject.Database;
import org.json.JSONObject;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MessageReactionDatabase {
public static boolean saveOrUpdateReaction(UUID messageId, UUID userId, String reaction) {
String sql = """
INSERT INTO message_reactions (message_id, user_id, emoji)
VALUES (?, ?, ?)
ON CONFLICT (message_id, user_id)
DO UPDATE SET emoji = EXCLUDED.emoji, reacted_at = CURRENT_TIMESTAMP
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
ps.setObject(2, userId);
ps.setString(3, reaction);
return ps.executeUpdate() > 0;
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
public static List<String> getReactions(UUID messageId) {
String sql = "SELECT emoji FROM message_reactions WHERE message_id = ?";
List<String> reactions = new ArrayList<>();
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
reactions.add(rs.getString("emoji"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return reactions;
}
public static JSONObject getCountsAsJson(UUID messageId) {
String sql = """
SELECT emoji, COUNT(*) AS c
FROM message_reactions
WHERE message_id = ?
GROUP BY emoji
""";
JSONObject counts = new JSONObject();
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String emoji = rs.getString("emoji");
int c = rs.getInt("c");
counts.put(emoji, c);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return counts; // مثال: {"❤️":2,"👍":1}
}
}
@@ -283,7 +283,36 @@ public class PrivateChatDatabase {
}
public static UUID findChatBetween(UUID user1, UUID user2) {
String sql = """
SELECT chat_id FROM private_chat
WHERE (user1_id = ? AND user2_id = ?) OR (user1_id = ? AND user2_id = ?)
""";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, user1);
ps.setObject(2, user2);
ps.setObject(3, user2);
ps.setObject(4, user1);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
return (UUID) rs.getObject("chat_id");
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static UUID getOtherParticipant(UUID chatId, UUID me) {
List<UUID> members = getMembers(chatId);
for (UUID u : members) {
if (!u.equals(me)) return u;
}
return null;
}
}
@@ -40,5 +40,4 @@ public class Contact {
}
@@ -1,6 +1,7 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import org.json.JSONObject;
import java.util.UUID;
public class ContactEntry {
@@ -71,4 +72,16 @@ public class ContactEntry {
public String toString() {
return profileName + " (@" + contact_displayId + ")" + (isBlocked ? " [Blocked]" : "") + " Last Seen:" + (lastSeenTime != null ? " " + lastSeenTime.toString() : "");
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
obj.put("contact_id", contactId.toString());
obj.put("user_id", userId);
obj.put("profile_name", profileName);
obj.put("image_url", imageUrl);
obj.put("is_blocked", isBlocked);
return obj;
}
}
@@ -2,6 +2,7 @@ package org.to.telegramfinalproject.Models;
import org.json.JSONArray;
import org.json.JSONObject;
import org.to.telegramfinalproject.Database.userDatabase;
import java.time.LocalDateTime;
import java.util.List;
@@ -9,20 +10,43 @@ import java.util.UUID;
public class JsonUtil {
public static JSONArray contactListToJson(List<Contact> contacts) {
JSONArray array = new JSONArray();
for (Contact contact : contacts) {
UUID contactId = contact.getContact_id();
User contactUser = userDatabase.findByInternalUUID(contactId);
if (contactUser == null) continue;
JSONObject obj = new JSONObject();
obj.put("user_id", contact.getUser_id().toString());
obj.put("contact_id", contact.getContact_id().toString());
obj.put("contact_id", contactId.toString()); // UUID
obj.put("user_id", contactUser.getUser_id()); // public ID
obj.put("profile_name", contactUser.getProfile_name());
obj.put("image_url", contactUser.getImage_url());
obj.put("is_blocked", contact.getIs_blocked());
obj.put("added_at", contact.getAdd_at().toString());
array.put(obj);
}
return array;
}
public static JSONArray contactEntryListToJson(List<ContactEntry> contactEntries) {
JSONArray array = new JSONArray();
for (ContactEntry entry : contactEntries) {
JSONObject obj = new JSONObject();
obj.put("contact_id", entry.getContactId().toString());
obj.put("user_id", entry.getUserId());
obj.put("profile_name", entry.getProfileName());
obj.put("image_url", entry.getImageUrl());
obj.put("is_blocked", entry.isBlocked());
array.put(obj);
}
return array;
}
@@ -1,18 +1,19 @@
package org.to.telegramfinalproject.Models;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
public class Message {
private final UUID message_id;
private UUID message_id;
private UUID sender_id;
private String receiver_type; // private, group, channel
private String receiver_type;
private UUID receiver_id;
private String content;
private String message_type; // TEXT, IMAGE, FILE, ...
private String message_type;
private LocalDateTime send_at;
private String status; // SENT, DELIVERED, READ
private String status;
private UUID reply_to_id;
private boolean is_edited;
private UUID original_message_id;
@@ -21,6 +22,30 @@ public class Message {
private List<FileAttachment> attachments;
private boolean is_deleted_globally;
private LocalDateTime edited_at;
private transient String sender_name;
private transient String receiver_name;
// Full Constructor
public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content,
String message_type, LocalDateTime send_at, String status,
UUID reply_to_id, boolean is_edited, boolean is_deleted_globally,
UUID original_message_id, UUID forwarded_by, UUID forwarded_from) {
this.message_id = message_id;
this.sender_id = sender_id;
this.receiver_type = receiver_type;
this.receiver_id = receiver_id;
this.content = content;
this.message_type = message_type;
this.send_at = send_at;
this.status = status;
this.reply_to_id = reply_to_id;
this.is_edited = is_edited;
this.is_deleted_globally = is_deleted_globally;
this.original_message_id = original_message_id;
this.forwarded_by = forwarded_by;
this.forwarded_from = forwarded_from;
}
public Message(UUID message_id, UUID sender_id, String receiver_type, UUID receiver_id, String content,
String message_type, LocalDateTime send_at, String status,
@@ -43,123 +68,134 @@ public class Message {
this.edited_at = edited_at;
}
public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType, String content, String messageType, LocalDateTime now) {
// Short Constructors
//for normal messages
public Message(UUID messageId, UUID senderId, UUID receiverId, String receiverType,
String content, String messageType, LocalDateTime sendAt) {
this.message_id = messageId;
this.sender_id = senderId;
this.receiver_id = receiverId;
this.receiver_type = receiverType;
this.content = content;
this.message_type = messageType;
this.send_at = now;
this.send_at = sendAt;
}
public UUID getMessage_id() {
return message_id;
}
public UUID getSender_id() {
return sender_id;
}
public void setSender_id(UUID sender_id) {
this.sender_id = sender_id;
}
public String getReceiver_type() {
return receiver_type;
}
public void setReceiver_type(String receiver_type) {
this.receiver_type = receiver_type;
}
public UUID getReceiver_id() {
return receiver_id;
}
public void setReceiver_id(UUID receiver_id) {
this.receiver_id = receiver_id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
//for reply messages
public Message(UUID messageId, UUID senderId, String receiverType, UUID receiverId,
String content, String messageType, LocalDateTime sendAt, String status,
UUID replyToId) {
this.message_id = messageId;
this.sender_id = senderId;
this.receiver_type = receiverType;
this.receiver_id = receiverId;
this.content = content;
}
public String getMessage_type() {
return message_type;
}
public void setMessage_type(String message_type) {
this.message_type = message_type;
}
public LocalDateTime getSend_at() {
return send_at;
}
public void setSend_at(LocalDateTime send_at) {
this.send_at = send_at;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.message_type = messageType;
this.send_at = sendAt;
this.status = status;
this.reply_to_id = replyToId;
this.is_edited = false;
this.is_deleted_globally = false;
}
public UUID getReply_to_id() {
return reply_to_id;
public Message(UUID messageId, UUID senderId, String receiverType, UUID receiverId,
String content, String messageType, LocalDateTime sendAt, String status,
UUID replyToId, boolean isEdited, boolean isDeletedGlobally,
UUID originalMessageId, UUID forwardedBy, UUID forwardedFrom,
LocalDateTime editedAt) {
this.message_id = messageId;
this.sender_id = senderId;
this.receiver_type = receiverType;
this.receiver_id = receiverId;
this.content = content;
this.message_type = messageType;
this.send_at = sendAt;
this.status = status;
this.reply_to_id = replyToId;
this.is_edited = isEdited;
this.is_deleted_globally = isDeletedGlobally;
this.original_message_id = originalMessageId;
this.forwarded_by = forwardedBy;
this.forwarded_from = forwardedFrom;
this.edited_at = editedAt;
}
public void setReply_to_id(UUID reply_to_id) {
this.reply_to_id = reply_to_id;
// Getters & Setters
public UUID getMessage_id() { return message_id; }
public void setMessage_id(UUID messageId) { this.message_id = messageId; }
public UUID getSender_id() { return sender_id; }
public void setSender_id(UUID sender_id) { this.sender_id = sender_id; }
public String getReceiver_type() { return receiver_type; }
public void setReceiver_type(String receiver_type) { this.receiver_type = receiver_type; }
public UUID getReceiver_id() { return receiver_id; }
public void setReceiver_id(UUID receiver_id) { this.receiver_id = receiver_id; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getMessage_type() { return message_type; }
public void setMessage_type(String message_type) { this.message_type = message_type; }
public LocalDateTime getSend_at() { return send_at; }
public void setSend_at(LocalDateTime send_at) { this.send_at = send_at; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public UUID getReply_to_id() { return reply_to_id; }
public void setReply_to_id(UUID reply_to_id) { this.reply_to_id = reply_to_id; }
public boolean isIs_edited() { return is_edited; }
public void setIs_edited(boolean is_edited) { this.is_edited = is_edited; }
public boolean isIs_deleted_globally() { return is_deleted_globally; }
public void setIs_deleted_globally(boolean is_deleted_globally) {
this.is_deleted_globally = is_deleted_globally;
}
public boolean isIs_edited() {
return is_edited;
public UUID getOriginal_message_id() { return original_message_id; }
public void setOriginal_message_id(UUID original_message_id) { this.original_message_id = original_message_id; }
public UUID getForwarded_by() { return forwarded_by; }
public void setForwarded_by(UUID forwarded_by) { this.forwarded_by = forwarded_by; }
public UUID getForwarded_from() { return forwarded_from; }
public void setForwarded_from(UUID forwarded_from) { this.forwarded_from = forwarded_from; }
public List<FileAttachment> getAttachments() { return attachments; }
public void setAttachments(List<FileAttachment> attachments) { this.attachments = attachments; }
public String getSender_name() {
return sender_name;
}
public void setIs_edited(boolean is_edited) {
this.is_edited = is_edited;
public void setSender_name(String sender_name) {
this.sender_name = sender_name;
}
public UUID getOriginal_message_id() {
return original_message_id;
public String getReceiver_name() {
return receiver_name;
}
public void setOriginal_message_id(UUID original_message_id) {
this.original_message_id = original_message_id;
public void setReceiver_name(String receiver_name) {
this.receiver_name = receiver_name;
}
public UUID getForwarded_by() {
return forwarded_by;
public LocalDateTime getEdited_at() {
return edited_at;
}
public void setForwarded_by(UUID forwarded_by) {
this.forwarded_by = forwarded_by;
}
public UUID getForwarded_from() {
return forwarded_from;
}
public void setForwarded_from(UUID forwarded_from) {
this.forwarded_from = forwarded_from;
}
public void setAttachments(List<FileAttachment> attachments) {
this.attachments = attachments;
}
public List<FileAttachment> getAttachments() {
return attachments;
public void setEdited_at(LocalDateTime edited_at) {
this.edited_at = edited_at;
}
public void setIs_deleted_globally(boolean is_deleted_globally) {
@@ -427,10 +427,14 @@ public class ClientHandler implements Runnable {
JSONObject obj = new JSONObject();
obj.put("type", "message");
obj.put("content", m.getContent());
obj.put("sender", m.getSender_id().toString());
obj.put("time", m.getSend_at().toString());
obj.put("sender", m.getSender_id() != null ? m.getSender_id().toString() : "N/A");
obj.put("receiver_id", m.getReceiver_id().toString());
obj.put("receiver_type", m.getReceiver_type());
User senderUser = userDatabase.findByInternalUUID(m.getSender_id());
String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown";
obj.put("sender_name", senderName);
results.add(obj);
}
@@ -447,13 +451,23 @@ public class ClientHandler implements Runnable {
}
List<Message> groupMessages = MessageDatabase.searchMessagesInGroups(groupIds, keyword);
List<Message> groupMessages = MessageDatabase.searchMessagesInGroups(groupIds, keyword, currentUser.getInternal_uuid());
for (Message m : groupMessages) {
JSONObject obj = new JSONObject();
obj.put("type", "message");
obj.put("from", "group");
obj.put("content", m.getContent());
obj.put("time", m.getSend_at().toString());
obj.put("sender", m.getSender_id() != null ? m.getSender_id().toString() : "N/A");
obj.put("receiver_id", m.getReceiver_id().toString());
obj.put("receiver_type", "group");
Group senderGroup = GroupDatabase.findByInternalUUID(m.getReceiver_id());
String groupName = senderGroup != null ? senderGroup.getGroup_name() : "Unknown";
obj.put("group_name", groupName);
User senderUser = userDatabase.findByInternalUUID(m.getSender_id());
String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown";
obj.put("sender_name", senderName);
results.add(obj);
}
@@ -464,6 +478,15 @@ public class ClientHandler implements Runnable {
obj.put("from", "channel");
obj.put("content", m.getContent());
obj.put("time", m.getSend_at().toString());
obj.put("sender", m.getSender_id() != null ? m.getSender_id().toString() : "N/A");
obj.put("receiver_id", m.getReceiver_id().toString());
obj.put("receiver_type", "channel");
Channel SenderChannel = ChannelDatabase.findByInternalUUID(m.getReceiver_id());
String channelName = SenderChannel != null ? SenderChannel.getChannel_name() : "Unknown";
obj.put("channel_name", channelName);
User senderUser = userDatabase.findByInternalUUID(m.getSender_id());
String senderName = senderUser != null ? senderUser.getProfile_name() : "Unknown";
obj.put("sender_name", senderName);
results.add(obj);
}
@@ -494,6 +517,7 @@ public class ClientHandler implements Runnable {
currentUser.getImage_url(),
contactUUID
);
}
response = success
@@ -1280,7 +1304,7 @@ public class ClientHandler implements Runnable {
break;
}
messages = MessageDatabase.privateChatHistory(chatId);
messages = MessageDatabase.privateChatHistory(chatId, currentUser.getInternal_uuid());
}
@@ -1290,7 +1314,7 @@ public class ClientHandler implements Runnable {
response = new ResponseModel("error", "Group not found.");
break;
}
messages = MessageDatabase.groupChatHistory(group.getInternal_uuid());
messages = MessageDatabase.groupChatHistory(group.getInternal_uuid(),currentUser.getInternal_uuid());
}
@@ -1300,7 +1324,7 @@ public class ClientHandler implements Runnable {
response = new ResponseModel("error", "Channel not found.");
break;
}
messages = MessageDatabase.channelChatHistory(channel.getInternal_uuid());
messages = MessageDatabase.channelChatHistory(channel.getInternal_uuid(),currentUser.getInternal_uuid());
}
default -> {
response = new ResponseModel("error", "Invalid receiver type.");
@@ -2003,7 +2027,6 @@ public class ClientHandler implements Runnable {
}
break;
case "get_or_create_private_chat": {
if (currentUser == null) {
response = new ResponseModel("error", "Unauthorized. Please login first.");
@@ -2014,7 +2037,23 @@ public class ClientHandler implements Runnable {
UUID user1 = UUID.fromString(requestJson.getString("user1"));
UUID user2 = UUID.fromString(requestJson.getString("user2"));
UUID oldChat = PrivateChatDatabase.findChatBetween(user1, user2);
UUID chatId = PrivateChatDatabase.getOrCreateChat(user1, user2);
boolean isNew = oldChat == null;
if (isNew) {
JSONObject chatPayload = new JSONObject();
chatPayload.put("action", "created_private_chat");
JSONObject chatData = new JSONObject();
chatData.put("chat_id", chatId.toString());
chatData.put("chat_type", "private");
chatData.put("last_message_time", LocalDateTime.now().toString());
chatPayload.put("data", chatData);
RealTimeEventDispatcher.sendToUser(user1, chatPayload);
RealTimeEventDispatcher.sendToUser(user2, chatPayload);
}
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
@@ -2029,6 +2068,7 @@ public class ClientHandler implements Runnable {
case "get_private_chat_target": {
UUID chatId = UUID.fromString(requestJson.getString("chat_id"));
userId = currentUser.getInternal_uuid();
@@ -2044,6 +2084,346 @@ public class ClientHandler implements Runnable {
break;
}
case "get_contact_list": {
if (currentUser == null) {
response = new ResponseModel("error", "Unauthorized. Please login first.");
break;
}
List<Contact> contacts = ContactDatabase.getContacts(currentUser.getInternal_uuid());
List<ContactEntry> contactEntries = new ArrayList<>();
for (Contact contact : contacts) {
UUID contactId = contact.getContact_id();
User contactUser = userDatabase.findByInternalUUID(contactId);
if (contactUser == null) continue;
ContactEntry entry = new ContactEntry(
contactId,
contactUser.getUser_id(),
contactUser.getProfile_name(),
contactUser.getImage_url(),
contact.getIs_blocked()
);
contactEntries.add(entry);
}
// Optional: sort alphabetically
contactEntries.sort(Comparator.comparing(ContactEntry::getProfileName, String.CASE_INSENSITIVE_ORDER));
JSONObject data = new JSONObject();
data.put("contact_list", JsonUtil.contactEntryListToJson(contactEntries));
response = new ResponseModel("success", "Contact list refreshed", data);
break;
}
case "get_chat_messages" : {
UUID chatId = UUID.fromString(requestJson.getString("chat_id"));
String chatType = requestJson.getString("chat_type");
int offset = requestJson.optInt("offset", 0);
int limit = requestJson.optInt("limit", 10);
List<Message> messages = MessageDatabase.getMessagesForChat(chatId, chatType, currentUser.getInternal_uuid(), offset, limit);
JSONArray result = new JSONArray();
for (Message m : messages) {
JSONObject obj = new JSONObject();
obj.put("message_id", m.getMessage_id().toString());
obj.put("sender_id", m.getSender_id().toString());
User sender = userDatabase.findByInternalUUID(m.getSender_id());
obj.put("sender_name", sender != null ? sender.getProfile_name() : "Unknown");
obj.put("receiver_id", m.getReceiver_id().toString());
obj.put("receiver_type", m.getReceiver_type());
String receiverName = switch (m.getReceiver_type()) {
case "group" -> {
Group g = GroupDatabase.findByInternalUUID(m.getReceiver_id());
yield g != null ? g.getGroup_name() : "Unknown group";
}
case "channel" -> {
Channel c = ChannelDatabase.findByInternalUUID(m.getReceiver_id());
yield c != null ? c.getChannel_name() : "Unknown channel";
}
case "private" -> {
UUID otherId = PrivateChatDatabase.getOtherParticipant(
m.getReceiver_id(), // chat_id
currentUser.getInternal_uuid() // my user uuid
);
User other = userDatabase.findByInternalUUID(otherId);
yield other != null ? other.getProfile_name() : "Unknown user";
}
default -> "Unknown";
};
obj.put("receiver_name", receiverName);
obj.put("content", m.getContent());
obj.put("message_type", m.getMessage_type());
obj.put("time", m.getSend_at().toString());
obj.put("is_edited", m.isIs_edited());
obj.put("is_deleted_globally", m.isIs_deleted_globally());
obj.put("edited_at", m.getEdited_at() != null ? m.getEdited_at().toString() : JSONObject.NULL);
//For reply messages
if (m.getReply_to_id() != null) {
Message replied = MessageDatabase.findById(m.getReply_to_id());
if (replied != null) {
User repliedSender = userDatabase.findByInternalUUID(replied.getSender_id());
obj.put("reply_to_id", replied.getMessage_id().toString());
obj.put("reply_to_sender", repliedSender != null ? repliedSender.getProfile_name() : "Unknown");
obj.put("reply_to_content", replied.getContent());
}
}
// For forwarded messages
if (m.getOriginal_message_id() != null && m.getForwarded_from() != null) {
User originalSender = userDatabase.findByInternalUUID(m.getForwarded_from());
obj.put("is_forwarded", true);
obj.put("forwarded_from_id", m.getForwarded_from().toString());
obj.put("forwarded_from_name", originalSender != null ? originalSender.getProfile_name() : "Unknown");
} else {
obj.put("is_forwarded", false);
}
//For reactions
List<String> reactions = MessageReactionDatabase.getReactions(m.getMessage_id());
obj.put("reactions", new JSONArray(reactions));
result.put(obj);
}
JSONObject data = new JSONObject();
data.put("get_chat_messages", result);
response = new ResponseModel("success", "get messages ", data);
break;
}
case "delete_message" : {
UUID messageId = UUID.fromString(requestJson.getString("message_id"));
String deleteType = requestJson.getString("delete_type");
Message msg = MessageDatabase.findById(messageId);
if (msg == null) {
response = new ResponseModel("error", "Message not found.");
break;
}
UUID currentUserId = currentUser.getInternal_uuid();
if (deleteType.equals("one-sided")) {
boolean success = MessageDatabase.markAsDeletedForUser(messageId, currentUserId);
if (success)
response = new ResponseModel("success", "Message deleted for current user.");
else
response = new ResponseModel("error", "Failed to delete message for user.");
break;
}
//If allowed to delete the message
if (deleteType.equals("global")) {
boolean allowed = false;
String type = msg.getReceiver_type();
UUID chatId = msg.getReceiver_id();
if (type.equals("private") || type.equals("group")) {
allowed = msg.getSender_id().equals(currentUserId);
} else if (type.equals("channel")) {
//Only owner and admins can delete messages
allowed = ChannelPermissionUtil.canDeleteMessage(chatId, currentUserId);
}
if (!allowed) {
response = new ResponseModel("error", "You are not allowed to delete this message globally.");
break;
}
boolean success = MessageDatabase.markAsGloballyDeleted(messageId);
if (success) {
response = new ResponseModel("success", "Message deleted globally.");
Message updated = MessageDatabase.findById(messageId);
List<UUID> receivers = Receivers.resolveFor(updated.getReceiver_type(), updated.getReceiver_id(), null);
RealTimeEventDispatcher.notifyMessageDeletedGlobal(updated.getReceiver_id(), messageId, receivers);
}
else{
response = new ResponseModel("error", "Failed to delete message globally.");
}
break;
}
response = new ResponseModel("error", "Invalid delete_type.");
break;
}
case "edit_message" : {
UUID msgId = UUID.fromString(requestJson.getString("message_id"));
String newContent = requestJson.getString("new_content");
Message msg = MessageDatabase.findById(msgId);
if (msg == null) {
response = new ResponseModel("error", "Message not found.");
break;
}
//Only sender can edit
if (!msg.getSender_id().equals(currentUser.getInternal_uuid())) {
response = new ResponseModel("error", "You can only edit your own messages.");
break;
}
boolean success = MessageDatabase.updateContentAndMarkEdited(msgId, newContent);
if (!success) {
response = new ResponseModel("error", "Failed to update message.");
} else {
response = new ResponseModel("success", "Message edited.");
Message updated = MessageDatabase.findById(msgId);
List<UUID> receivers = Receivers.resolveFor(updated.getReceiver_type(), updated.getReceiver_id(), null);
RealTimeEventDispatcher.notifyMessageEdited(updated.getReceiver_id(), updated.getMessage_id(), newContent, LocalDateTime.now(), receivers);
}
break;
}
case "send_reply_message" : {
UUID senderId = currentUser.getInternal_uuid();
String content = requestJson.getString("content");
String receiverType = requestJson.getString("receiver_type");
UUID receiverId = UUID.fromString(requestJson.getString("receiver_id"));
//For block
if ("private".equals(receiverType) && !canSendToPrivate(receiverId, senderId)) {
response = new ResponseModel("error", "You can't message this user (blocked).");
break;
}
UUID replyToId = UUID.fromString(requestJson.getString("reply_to_id"));
Message message = new Message(
UUID.randomUUID(),
senderId,
receiverType,
receiverId,
content,
"TEXT",
LocalDateTime.now(),
"SEND",
replyToId
);
boolean saved = MessageDatabase.saveReplyMessage(message);
String excerpt = MessageDatabase.getExcerpt(replyToId);
JSONObject meta = new JSONObject().put("reply_to", new JSONObject()
.put("id", replyToId.toString())
.put("excerpt", excerpt));
List<UUID> receivers = Receivers.resolveFor(receiverType, receiverId, senderId);
//RealTimeEventDispatcher.sendNewMessage(message, receivers, "reply", meta);
RealTimeEventDispatcher.sendNewMessageFiltered(message, receivers, senderId, "reply", meta);
response = saved ?
new ResponseModel("success", "Reply sent") :
new ResponseModel("error", "Failed to send reply");
break;
}
case "forward_message" : {
UUID senderId = currentUser.getInternal_uuid();
UUID originalMessageId = UUID.fromString(requestJson.getString("original_message_id"));
UUID targetChatId = UUID.fromString(requestJson.getString("target_chat_id"));
String targetChatType = requestJson.getString("target_chat_type");
//For block
if ("private".equals(targetChatType) && !canSendToPrivate(targetChatId, senderId)) {
response = new ResponseModel("error", "You can't message this user (blocked).");
break;
}
// original message
Message original = MessageDatabase.findById(originalMessageId);
if (original == null) {
response = new ResponseModel("error", "Original message not found.");
break;
}
//make forwarded message
Message forwarded = new Message(
UUID.randomUUID(),
currentUser.getInternal_uuid(),
targetChatType,
targetChatId,
original.getContent(),
original.getMessage_type(),
LocalDateTime.now(),
"SEND",
null, // reply_to_id
false, // is_edited
false, // is_deleted_globally
original.getMessage_id(), //original message id
currentUser.getInternal_uuid(), // forwarded_by
original.getSender_id(), // forwarded_from
null // edited_at
);
boolean success = MessageDatabase.saveForwardedMessage(forwarded);
if (success) {
response = new ResponseModel("success", "Message forwarded.");
JSONObject meta = new JSONObject().put("forwarded_from", new JSONObject()
.put("chat_id", original.getReceiver_id().toString())
.put("message_id", original.getMessage_id().toString())
.put("sender_id", original.getSender_id().toString())
.put("sender_name", userDatabase.findByInternalUUID(original.getSender_id()).getProfile_name()));
List<UUID> receivers = Receivers.resolveFor(targetChatType, targetChatId, currentUser.getInternal_uuid());
//RealTimeEventDispatcher.sendNewMessage(forwarded, receivers, "forward", meta);
RealTimeEventDispatcher.sendNewMessageFiltered(forwarded, receivers, senderId, "forward", meta);
} else {
response = new ResponseModel("error", "Failed to forward message.");
}
break;
}
case "react_to_message": {
UUID messageId = UUID.fromString(requestJson.getString("message_id"));
String reaction = requestJson.getString("reaction");
boolean success = MessageReactionDatabase.saveOrUpdateReaction(messageId, currentUser.getInternal_uuid(), reaction);
if (success) {
Message msg = MessageDatabase.findById(messageId);
JSONObject counts = MessageReactionDatabase.getCountsAsJson(messageId);
List<UUID> receivers = Receivers.resolveFor(msg.getReceiver_type(), msg.getReceiver_id(), null);
RealTimeEventDispatcher.notifyReactionAdded(
msg.getReceiver_id(), // chatId
messageId, // messageId
reaction, // emoji
counts.optInt(reaction, 0),
counts,
receivers
);
response = new ResponseModel("success", "Reaction saved.");
} else {
response = new ResponseModel("error", "Failed to save reaction.");
}
break;
}
case "get_user_profile": {
// Get the current user's UUID
UUID user_UUID = this.currentUser.getInternal_uuid();
@@ -2122,8 +2502,8 @@ public class ClientHandler implements Runnable {
}
case "send_saved_messages": {
response = SidebarService.handleSendMessage(requestJson);
break;
response = SidebarService.handleSendMessage(requestJson);
break;
}
case "search_contacts": {
@@ -2213,6 +2593,13 @@ public class ClientHandler implements Runnable {
if(Objects.equals(receiverType, "private")){
PrivateChatDatabase.clearDeletedFlag(senderId, receiverId);
UUID other = PrivateChatDatabase.getOtherParticipant(receiverId, senderId);
if (other == null) {
return new ResponseModel("error", "Invalid private chat.");
}
if (ContactDatabase.isBlocked(senderId, other) || ContactDatabase.isBlocked(other, senderId)) {
return new ResponseModel("error", "You can't message this user (blocked).");
}
}
@@ -2320,6 +2707,14 @@ public class ClientHandler implements Runnable {
}
}
// helper
private boolean canSendToPrivate(UUID chatId, UUID senderId) {
UUID other = PrivateChatDatabase.getOtherParticipant(chatId, senderId);
if (other == null) return false;
return !(ContactDatabase.isBlocked(senderId, other) || ContactDatabase.isBlocked(other, senderId));
}
}
@@ -2,6 +2,7 @@ package org.to.telegramfinalproject.Server;
import org.json.JSONObject;
import org.to.telegramfinalproject.Database.ChannelDatabase;
import org.to.telegramfinalproject.Database.ContactDatabase;
import org.to.telegramfinalproject.Database.GroupDatabase;
import org.to.telegramfinalproject.Database.userDatabase;
import org.to.telegramfinalproject.Models.Message;
@@ -81,33 +82,119 @@ public class RealTimeEventDispatcher {
public static void notifyMessageEdited(UUID messageId, String newContent, List<UUID> receivers) {
String editTime = LocalDateTime.now().toString();
public static void notifyMessageEdited(UUID chatId, UUID messageId, String newContent, LocalDateTime editedAt, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
data.put("new_content", newContent);
data.put("edited_at", editTime);
data.put("edited_at", editedAt.toString());
JSONObject event = new JSONObject();
event.put("action", "edit_message");
event.put("action", "message_edited");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void notifyMessageDeleted(UUID messageId, List<UUID> receivers) {
public static void sendNewMessage(Message message, List<UUID> receivers, String kind, JSONObject meta) {
JSONObject payload = new JSONObject();
payload.put("action", "new_message");
JSONObject data = new JSONObject();
data.put("id", message.getMessage_id().toString());
data.put("chat_id", message.getReceiver_id().toString());
data.put("sender_id", message.getSender_id().toString());
data.put("receiver_id", message.getReceiver_id().toString());
data.put("receiver_type", message.getReceiver_type());
data.put("content", message.getContent());
data.put("message_type", message.getMessage_type());
data.put("send_at", message.getSend_at().toString());
User sender = userDatabase.findByInternalUUID(message.getSender_id());
if (sender != null) data.put("sender_name", sender.getProfile_name());
data.put("kind", kind == null ? "plain" : kind); // plain|reply|forward
if (meta != null) data.put("meta", meta); // reply_to {...} | forwarded_from {...}
payload.put("data", data);
for (UUID userId : receivers) sendToUser(userId, payload);
}
public static void sendNewMessageFiltered(Message m, List<UUID> receivers, UUID senderId, String kind, JSONObject meta) {
JSONObject payload = new JSONObject()
.put("action", "new_message")
.put("data", new JSONObject()
.put("id", m.getMessage_id().toString())
.put("message_id", m.getMessage_id().toString())
.put("sender_id", m.getSender_id().toString())
.put("sender_name", userDatabase.findByInternalUUID(m.getSender_id()).getProfile_name())
.put("receiver_id", m.getReceiver_id().toString())
.put("receiver_type", m.getReceiver_type())
.put("content", m.getContent())
.put("message_type", m.getMessage_type())
.put("send_at", m.getSend_at().toString())
.put("kind", kind)
.put("meta", meta != null ? meta : JSONObject.NULL)
);
for (UUID uid : receivers) {
//If receiver blocked
if ("private".equalsIgnoreCase(m.getReceiver_type()) && ContactDatabase.isBlocked(uid, senderId)) continue;
sendToUser(uid, payload);
}
}
public static void notifyMessageDeletedGlobal(UUID chatId, UUID messageId, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
JSONObject event = new JSONObject();
event.put("action", "delete_message");
event.put("action", "message_deleted_global");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void notifyReactionAdded(UUID chatId, UUID messageId, String emoji,
int totalForEmoji, JSONObject countsAll, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
data.put("emoji", emoji);
data.put("counts", countsAll); // {"❤️":3,"👍":1,...}
data.put("count_for_emoji", totalForEmoji);
JSONObject event = new JSONObject();
event.put("action", "message_reacted");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void notifyReactionRemoved(UUID chatId, UUID messageId, String emoji,
int totalForEmoji, JSONObject countsAll, List<UUID> receivers) {
JSONObject data = new JSONObject();
data.put("chat_id", chatId.toString());
data.put("message_id", messageId.toString());
data.put("emoji", emoji);
data.put("counts", countsAll);
data.put("count_for_emoji", totalForEmoji);
JSONObject event = new JSONObject();
event.put("action", "message_unreacted");
event.put("data", data);
broadcastToUsers(receivers, event);
}
public static void notifyUserUpdated(UUID userId, String newProfileName, String newImageUrl, List<UUID> contactIds) {
JSONObject data = new JSONObject();
data.put("user_id", userId.toString());
@@ -328,6 +415,13 @@ public class RealTimeEventDispatcher {
for (UUID userId : receivers) {
sendToUser(userId, payload);
}
for (UUID uid : receivers) {
//If receiver blocked
if ("private".equalsIgnoreCase(message.getReceiver_type()) && ContactDatabase.isBlocked(uid, message.getSender_id())) continue;
sendToUser(uid, payload);
}
}
@@ -0,0 +1,21 @@
package org.to.telegramfinalproject.Server;
import org.to.telegramfinalproject.Database.ChannelDatabase;
import org.to.telegramfinalproject.Database.GroupDatabase;
import org.to.telegramfinalproject.Database.PrivateChatDatabase;
import java.util.List;
import java.util.UUID;
public class Receivers {
public static List<UUID> resolveFor(String type, UUID chatId, UUID exclude) {
List<UUID> ids = switch (type) {
case "private" -> PrivateChatDatabase.getMembers(chatId);
case "group" -> GroupDatabase.getMemberUUIDs(chatId);
case "channel" -> ChannelDatabase.getSubscriberUUIDs(chatId);
default -> List.of();
};
if (exclude != null) ids.remove(exclude);
return ids;
}
}
@@ -2,7 +2,12 @@ package org.to.telegramfinalproject.Utils;
import org.json.JSONObject;
import org.to.telegramfinalproject.Database.ChannelDatabase;
import org.to.telegramfinalproject.Database.ConnectionDb;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
public class ChannelPermissionUtil {
@@ -51,4 +56,21 @@ public class ChannelPermissionUtil {
}
return false;
}
public static boolean canDeleteMessage(UUID channelId, UUID userId) {
String roleSql = "SELECT role FROM channel_subscribers WHERE channel_id = ? AND user_id = ?";
try (Connection conn = ConnectionDb.connect();
PreparedStatement ps = conn.prepareStatement(roleSql)) {
ps.setObject(1, channelId);
ps.setObject(2, userId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
String role = rs.getString("role");
return role.equalsIgnoreCase("owner") || role.equalsIgnoreCase("admin");
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
}
+10 -2
View File
@@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS messages (
content TEXT,
message_type VARCHAR(20) DEFAULT 'TEXT' CHECK (message_type IN ('TEXT','IMAGE','FILE','VIDEO','AUDIO','STICKER','GIF')),
send_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'SEND', -- SEND,DELIVERED,READ
status VARCHAR(20) DEFAULT 'SEND', -- SEND,DELIVERED,READ
reply_to_id UUID REFERENCES messages(message_id) ON DELETE SET NULL, --(Bouns)
is_edited BOOLEAN DEFAULT FALSE, --(Bonus)
edited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
@@ -145,7 +145,15 @@ CREATE TABLE IF NOT EXISTS message_attachments (
message_id UUID REFERENCES messages(message_id) ON DELETE CASCADE,
file_url TEXT NOT NULL,
file_type VARCHAR(20) CHECK (file_type IN ('IMAGE','VIDEO','AUDIO','FILE','GIF','STICKER')),
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
file_name TEXT,
file_size BIGINT,
mime_type TEXT,
width INT,
height INT,
duration_seconds INT,
thumbnail_url TEXT
);