From b3941108b5303229f9f7abf4f7166270d6a3a31a Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Mon, 11 Aug 2025 15:59:54 +0330 Subject: [PATCH] work on send file --- build.gradle | 2 - src/main/java/module-info.java | 6 +- .../Client/ActionHandler.java | 242 ++++++++++++++++-- .../Database/MessageDatabase.java | 8 +- .../telegramfinalproject/Models/Contact.java | 1 + .../Models/ContactEntry.java | 12 +- .../Models/FileAttachment.java | 131 +++++++--- .../telegramfinalproject/Models/Message.java | 20 ++ .../Server/ClientHandler.java | 229 +++++++++++++---- .../Server/MainServer.java | 1 + .../Server/TestServer.java | 57 +++++ .../Server/UploadHttp.java | 131 +++------- 12 files changed, 629 insertions(+), 211 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Server/TestServer.java diff --git a/build.gradle b/build.gradle index d17a5d0..2c9cfb2 100644 --- a/build.gradle +++ b/build.gradle @@ -46,8 +46,6 @@ dependencies { } //For upload files implementation 'com.sparkjava:spark-core:2.9.4' - implementation 'org.jcodec:jcodec:0.2.5' - implementation 'org.jcodec:jcodec-javase:0.2.5' implementation 'com.mpatric:mp3agic:0.9.1' //for mp3 implementation 'org.json:json:20231013' implementation 'org.kordamp.ikonli:ikonli-javafx:12.3.1' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2645769..274c4f7 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -11,12 +11,10 @@ module org.to.telegramfinalproject { requires eu.hansolo.tilesfx; requires org.json; requires java.sql; - requires javax.servlet.api; - requires spark.core; requires java.desktop; - requires jcodec; + requires spark.core; + requires javax.servlet.api; requires mp3agic; - requires jcodec.javase; opens org.to.telegramfinalproject to javafx.fxml; exports org.to.telegramfinalproject; exports org.to.telegramfinalproject.Client; diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 3c7e815..853dfdb 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -555,6 +555,7 @@ public class ActionHandler { ContactEntry entry = new ContactEntry( UUID.fromString(c.getString("contact_id")), c.getString("user_id"), + c.getString("contact_displayId"), c.getString("profile_name"), c.optString("image_url", ""), c.optBoolean("is_blocked", false) @@ -564,7 +565,6 @@ public class ActionHandler { - Session.activeChats = activeChats; Session.archivedChats = archivedChats; Session.chatList = chatList; @@ -899,6 +899,134 @@ public class ActionHandler { } } + + +// public void showContactList() { +// System.out.println("1. View All Contacts"); +// System.out.println("2. Search Contacts"); +// System.out.println("Choose an option: (0 to go back)"); +// int option = scanner.nextInt(); +// scanner.nextLine(); +// +// // Handle invalid input +// while (option < 0 || option > 2) { +// System.out.println("Invalid choice. Try again: "); +// option = scanner.nextInt(); +// scanner.nextLine(); +// } +// +// List contacts; +// +// if (option == 0) { +// return; +// } +// else if (option == 1) { +// contacts = Session.contactEntries; +// +// } else if (option == 2) { +// contacts = new ArrayList<>(); +// +// System.out.print("Enter name or user ID to search: "); +// String searchTerm = scanner.nextLine(); +// +// // Handle invalid input +// while (searchTerm.isEmpty()) { +// System.out.print("Search key can not be empty. Try again: "); +// searchTerm = scanner.nextLine(); +// } +// +// // Send a request to server +// JSONObject request = new JSONObject(); +// request.put("action", "search_contacts"); +// request.put("user_id", Session.getUserUUID()); +// request.put("search_term", searchTerm); +// +// JSONObject response = ActionHandler.sendWithResponse(request); +// +// if (!response.optString("status", "fail").equals("success")) { +// System.out.println("Failed to search contacts: " + response.optString("message", "Unknown error")); +// return; +// } +// +// JSONObject data = response.getJSONObject("data"); +// JSONArray contactsJson = data.getJSONArray("contacts"); +// +// for (int i = 0; i < contactsJson.length(); i++) { +// JSONObject contact = contactsJson.getJSONObject(i); +// +// UUID contactId = UUID.fromString(contact.getString("contact_id")); +// String userId = contact.getString("user_id"); +// String profileName = contact.getString("profile_name"); +// String imageUrl = contact.optString("image_url", ""); +// boolean isBlocked = contact.getBoolean("is_blocked"); +// String lastSeenString = contact.getString("last_seen"); +// LocalDateTime lastSeen = null; +// if (lastSeenString != null) { +// lastSeen = LocalDateTime.parse(lastSeenString); +// } +// +// contacts.add(new ContactEntry(contactId, userId, profileName, imageUrl, isBlocked)); +// } +// +// } else { +// System.out.println("❌ Invalid choice."); +// return; +// } +// +// if (contacts.isEmpty()) { +// System.out.println("📭 No contacts found."); +// return; +// } +// +// System.out.println("👥 Your Contacts:"); +// for (int i = 0; i < contacts.size(); i++) { +// System.out.println((i + 1) + ". " + contacts.get(i)); +// } +// +// System.out.print("Select a contact (0 to go back): "); +// int choice = scanner.nextInt(); +// scanner.nextLine(); +// +// if (choice == 0) return; +// if (choice < 1 || choice > contacts.size()) { +// System.out.println("❌ Invalid choice."); +// return; +// } +// +// ContactEntry selected = contacts.get(choice - 1); +// System.out.println("\n📇 What do you want to do with " + selected.getProfileName() + "?"); +// System.out.println("1. View Profile"); +// System.out.println("2. Send Message"); +// System.out.println("3. Remove Contact"); +// System.out.print("Enter your choice: "); +// int action = scanner.nextInt(); +// scanner.nextLine(); +// switch (action) { +// case 1 -> viewProfile(selected.getContactId()); +// case 2 -> startPrivateChat(selected); +// case 3 -> { +// // Send a request to server +// JSONObject request = new JSONObject(); +// request.put("action", "remove_contact"); +// request.put("user_id", Session.getUserUUID()); // Current user +// request.put("contact_id", selected.getContactId().toString()); // Contact to remove +// +// JSONObject response = ActionHandler.sendWithResponse(request); +// +// if ("success".equals(response.optString("status"))) { +// System.out.println("✅ Contact removed successfully."); +// Session.contactEntries.remove(selected); // Remove from local session list +// } else { +// System.out.println("❌ Failed to remove contact: " + +// response.optString("message", "Unknown error")); +// } +// } +// +// default -> System.out.println("❌ Invalid option."); +// } +// } + + private void viewProfile(UUID targetId) { JSONObject req = new JSONObject(); req.put("action", "view_profile"); @@ -3232,32 +3360,110 @@ public class ActionHandler { +// public void sendMessage(UUID chatId, String receiverType) { +// Scanner scanner = new Scanner(System.in); +// +// System.out.print("Enter your message: "); +// String content = scanner.nextLine(); +// +// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); +// String messageType = scanner.nextLine().toUpperCase(); +// Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); +// while (!allowedTypes.contains(messageType)) { +// System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE): "); +// messageType = scanner.nextLine().toUpperCase(); +// } +// +// JSONArray attachmentsArray = new JSONArray(); +// System.out.print("Do you want to attach files? (yes/no): "); +// if (scanner.nextLine().equalsIgnoreCase("yes")) { +// while (true) { +// System.out.print("File URL: "); +// String fileUrl = scanner.nextLine(); +// System.out.print("File Type (IMAGE / VIDEO / FILE): "); +// String fileType = scanner.nextLine().toUpperCase(); +// +// JSONObject fileJson = new JSONObject(); +// fileJson.put("file_url", fileUrl); +// fileJson.put("file_type", fileType); +// attachmentsArray.put(fileJson); +// +// System.out.print("Add another file? (yes/no): "); +// if (!scanner.nextLine().equalsIgnoreCase("yes")) break; +// } +// } +// +// // 🔹 فقط ارسال پیام با chat_id و receiver_type +// JSONObject messageJson = new JSONObject(); +// messageJson.put("action", "send_message"); +// messageJson.put("receiver_type", receiverType); +// messageJson.put("receiver_id", chatId.toString()); +// messageJson.put("content", content); +// messageJson.put("message_type", messageType); +// +// if (!attachmentsArray.isEmpty()) { +// messageJson.put("attachments", attachmentsArray); +// } +// +// JSONObject response = sendWithResponse(messageJson); +// if (response != null && response.getString("status").equals("success")) { +// System.out.println("✅ Message sent! ID: " + response.getJSONObject("data").getString("message_id")); +// } else { +// System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "No response")); +// } +// } + + + + public void sendMessage(UUID chatId, String receiverType) { Scanner scanner = new Scanner(System.in); - System.out.print("Enter your message: "); + System.out.print("Enter your message (leave empty if file only): "); String content = scanner.nextLine(); - System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); + System.out.print("Enter message type (TEXT / IMAGE / AUDIO / FILE / GIF): "); String messageType = scanner.nextLine().toUpperCase(); - Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); + Set allowedTypes = Set.of("TEXT", "IMAGE", "AUDIO", "FILE", "GIF"); while (!allowedTypes.contains(messageType)) { - System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE): "); + System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / AUDIO / FILE / GIF): "); messageType = scanner.nextLine().toUpperCase(); } JSONArray attachmentsArray = new JSONArray(); - System.out.print("Do you want to attach files? (yes/no): "); + System.out.print("Attach files? (yes/no): "); if (scanner.nextLine().equalsIgnoreCase("yes")) { while (true) { - System.out.print("File URL: "); - String fileUrl = scanner.nextLine(); - System.out.print("File Type (IMAGE / VIDEO / FILE): "); - String fileType = scanner.nextLine().toUpperCase(); + System.out.println("Paste the JSON you got from /upload (or leave empty to enter minimal fields):"); + String jsonLine = scanner.nextLine().trim(); + + JSONObject fileJson; + if (!jsonLine.isEmpty()) { + // انتظار خروجی کامل /upload + fileJson = new JSONObject(jsonLine); + // اگه خروجی /upload تو ریشه‌ست، تبدیلش کن به ساختار attachment + fileJson = new JSONObject() + .put("file_url", fileJson.optString("file_url", "")) + .put("file_type", fileJson.optString("file_type", "FILE")) + .put("file_name", fileJson.optString("file_name", "")) + .put("file_size", fileJson.optLong("file_size", 0)) + .put("mime_type", fileJson.optString("mime_type", "")) + .put("width", fileJson.isNull("width") ? JSONObject.NULL : fileJson.optInt("width")) + .put("height", fileJson.isNull("height") ? JSONObject.NULL : fileJson.optInt("height")) + .put("duration_seconds", fileJson.isNull("duration_seconds") ? JSONObject.NULL : fileJson.optInt("duration_seconds")) + .put("thumbnail_url", fileJson.isNull("thumbnail_url") ? JSONObject.NULL : fileJson.optString("thumbnail_url", null)); + } else { + // ورودی حداقلی + System.out.print("File URL: "); + String fileUrl = scanner.nextLine(); + System.out.print("File Type (IMAGE / AUDIO / FILE / GIF): "); + String fileType = scanner.nextLine().toUpperCase(); + + fileJson = new JSONObject(); + fileJson.put("file_url", fileUrl); + fileJson.put("file_type", fileType); + } - JSONObject fileJson = new JSONObject(); - fileJson.put("file_url", fileUrl); - fileJson.put("file_type", fileType); attachmentsArray.put(fileJson); System.out.print("Add another file? (yes/no): "); @@ -3265,15 +3471,13 @@ public class ActionHandler { } } - // 🔹 فقط ارسال پیام با chat_id و receiver_type JSONObject messageJson = new JSONObject(); messageJson.put("action", "send_message"); - messageJson.put("receiver_type", receiverType); - messageJson.put("receiver_id", chatId.toString()); + messageJson.put("receiver_type", receiverType); // "private"/"group"/"channel" + messageJson.put("receiver_id", chatId.toString()); // در private = chat_id messageJson.put("content", content); messageJson.put("message_type", messageType); - - if (!attachmentsArray.isEmpty()) { + if (attachmentsArray.length() > 0) { messageJson.put("attachments", attachmentsArray); } @@ -3281,7 +3485,7 @@ public class ActionHandler { if (response != null && response.getString("status").equals("success")) { System.out.println("✅ Message sent! ID: " + response.getJSONObject("data").getString("message_id")); } else { - System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "No response")); + System.out.println("❌ Failed to send message: " + (response != null ? response.optString("message","No message") : "No response")); } } diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 35e644d..72e691d 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -626,11 +626,13 @@ public class MessageDatabase { rs.getString("status"), (UUID) rs.getObject("reply_to_id"), rs.getBoolean("is_edited"), - rs.getBoolean("is_deleted_globally"), +// rs.getBoolean("is_deleted_globally"), (UUID) rs.getObject("original_message_id"), (UUID) rs.getObject("forwarded_by"), - (UUID) rs.getObject("forwarded_from") - ); + (UUID) rs.getObject("forwarded_from"), + rs.getBoolean("is_deleted_globally"), + (rs.getTimestamp("edited_at") != null) ? rs.getTimestamp("edited_at").toLocalDateTime() : null + ); } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Contact.java b/src/main/java/org/to/telegramfinalproject/Models/Contact.java index d85ae00..1240f21 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Contact.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Contact.java @@ -19,6 +19,7 @@ public class Contact { 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.added_at =add_at;} diff --git a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java index cd1e7e6..a61bc6e 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java +++ b/src/main/java/org/to/telegramfinalproject/Models/ContactEntry.java @@ -10,6 +10,7 @@ public class ContactEntry { private String profileName; private String imageUrl; private boolean isBlocked; + private String contact_displayId; public ContactEntry(UUID contactId, String userId, String profileName, String imageUrl, boolean isBlocked) { this.contactId = contactId; @@ -19,6 +20,15 @@ public class ContactEntry { this.isBlocked = isBlocked; } + public ContactEntry(UUID contactId, String userId,String contact_displayId , String profileName, String imageUrl, boolean isBlocked){ + this.contactId = contactId; + this.userId = userId; + this.contact_displayId = contact_displayId; + this.profileName = profileName; + this.imageUrl = imageUrl; + this.isBlocked = isBlocked; + } + public UUID getContactId() { return contactId; } @@ -41,7 +51,7 @@ public class ContactEntry { @Override public String toString() { - return profileName + " (" + userId + ")" + (isBlocked ? " [Blocked]" : ""); + return profileName + " ( @" + contact_displayId + ")" + (isBlocked ? " [Blocked]" : ""); } diff --git a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java index 8063ad4..58f7602 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java +++ b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java @@ -1,17 +1,19 @@ package org.to.telegramfinalproject.Models; +import org.json.JSONObject; import java.util.Objects; public class FileAttachment { - private String fileUrl; - private String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER - private String fileName; - private Long fileSize; - private String mimeType; // MIME type (example: image/png) - private Integer width; - private Integer height; - private Integer durationSeconds; //time for video and audio - private String thumbnailUrl; + + private final String fileUrl; + private final String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER + private final String fileName; + private final Long fileSize; + private final String mimeType; // e.g., image/png + private final Integer width; + private final Integer height; + private final Integer durationSeconds; // for audio/video + private final String thumbnailUrl; public FileAttachment(String fileUrl, String fileType, @@ -34,44 +36,91 @@ public class FileAttachment { } public FileAttachment(String fileUrl, String fileType) { - this.fileUrl = fileUrl; - this.fileType = fileType; + this(fileUrl, fileType, null, null, null, null, null, null, null); + } + + // ساخت از JSON /upload + public static FileAttachment fromUploadJson(JSONObject j) { + return new FileAttachment( + j.optString("file_url", ""), + j.optString("file_type", "FILE"), + emptyToNull(j.optString("file_name", null)), + j.has("file_size") && !j.isNull("file_size") ? j.getLong("file_size") : null, + emptyToNull(j.optString("mime_type", null)), + j.has("width") && !j.isNull("width") ? j.getInt("width") : null, + j.has("height") && !j.isNull("height") ? j.getInt("height") : null, + j.has("duration_seconds") && !j.isNull("duration_seconds") ? j.getInt("duration_seconds") : null, + j.isNull("thumbnail_url") ? null : emptyToNull(j.optString("thumbnail_url", null)) + ); + } + + // خروجی JSON برای RT/کلاینت + public JSONObject toJson() { + JSONObject out = new JSONObject() + .put("file_url", fileUrl) + .put("file_type", fileType); + + out.put("file_name", fileName == null ? JSONObject.NULL : fileName); + out.put("file_size", fileSize == null ? JSONObject.NULL : fileSize); + out.put("mime_type", mimeType == null ? JSONObject.NULL : mimeType); + out.put("width", width == null ? JSONObject.NULL : width); + out.put("height", height == null ? JSONObject.NULL : height); + out.put("duration_seconds", durationSeconds == null ? JSONObject.NULL : durationSeconds); + out.put("thumbnail_url", thumbnailUrl == null ? JSONObject.NULL : thumbnailUrl); + return out; + } + + // Helpers + public boolean isImage() { return "IMAGE".equalsIgnoreCase(fileType) || "GIF".equalsIgnoreCase(fileType); } + public boolean isAudio() { return "AUDIO".equalsIgnoreCase(fileType); } + public boolean hasDimensions() { return width != null && height != null; } + + private static String emptyToNull(String s) { + return (s == null || s.isBlank()) ? null : s; } // Getters - public String getFileUrl() { - return fileUrl; + public String getFileUrl() { return fileUrl; } + public String getFileType() { return fileType; } + public String getFileName() { return fileName; } + public Long getFileSize() { return fileSize; } + public String getMimeType() { return mimeType; } + public Integer getWidth() { return width; } + public Integer getHeight() { return height; } + public Integer getDurationSeconds() { return durationSeconds; } + public String getThumbnailUrl() { return thumbnailUrl; } + + // equals/hashCode/toString + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FileAttachment)) return false; + FileAttachment that = (FileAttachment) o; + return Objects.equals(fileUrl, that.fileUrl) && + Objects.equals(fileType, that.fileType) && + Objects.equals(fileName, that.fileName) && + Objects.equals(fileSize, that.fileSize) && + Objects.equals(mimeType, that.mimeType) && + Objects.equals(width, that.width) && + Objects.equals(height, that.height) && + Objects.equals(durationSeconds, that.durationSeconds) && + Objects.equals(thumbnailUrl, that.thumbnailUrl); } - public String getFileType() { - return fileType; + @Override public int hashCode() { + return Objects.hash(fileUrl, fileType, fileName, fileSize, mimeType, width, height, durationSeconds, thumbnailUrl); } - public String getFileName() { - return fileName; - } - - public Long getFileSize() { - return fileSize; - } - - public String getMimeType() { - return mimeType; - } - - public Integer getWidth() { - return width; - } - - public Integer getHeight() { - return height; - } - - public Integer getDurationSeconds() { - return durationSeconds; - } - - public String getThumbnailUrl() { - return thumbnailUrl; + @Override public String toString() { + return "FileAttachment{" + + "fileUrl='" + fileUrl + '\'' + + ", fileType='" + fileType + '\'' + + ", fileName='" + fileName + '\'' + + ", fileSize=" + fileSize + + ", mimeType='" + mimeType + '\'' + + ", width=" + width + + ", height=" + height + + ", durationSeconds=" + durationSeconds + + ", thumbnailUrl='" + thumbnailUrl + '\'' + + '}'; } } diff --git a/src/main/java/org/to/telegramfinalproject/Models/Message.java b/src/main/java/org/to/telegramfinalproject/Models/Message.java index 7f71b28..336998c 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/Message.java +++ b/src/main/java/org/to/telegramfinalproject/Models/Message.java @@ -48,6 +48,26 @@ public class Message { 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, + UUID reply_to_id, boolean is_edited, UUID original_message_id, + UUID forwarded_by, UUID forwarded_from,boolean is_deleted_globally, LocalDateTime edited_at) { + 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.original_message_id = original_message_id; + this.forwarded_by = forwarded_by; + this.forwarded_from = forwarded_from; + this.is_deleted_globally = is_deleted_globally; + this.edited_at = edited_at; + } // ✅ Short Constructors //for normal messages diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 47cce43..6ca7e2c 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -9,6 +9,7 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil; import java.io.*; import java.net.Socket; +import java.sql.Connection; import java.time.LocalDateTime; import java.util.*; @@ -111,6 +112,8 @@ public class ClientHandler implements Runnable { JSONObject c = new JSONObject(); c.put("user_id", contact.getUser_id().toString()); c.put("contact_id", contact.getContact_id().toString()); + User Contact = userDatabase.findByInternalUUID(contact.getContact_id()); + c.put("contact_displayId", Contact.getUser_id()); c.put("is_blocked", contact.getIs_blocked()); c.put("profile_name", target.getProfile_name()); @@ -2469,8 +2472,87 @@ public class ClientHandler implements Runnable { } - private ResponseModel handleSendMessage(JSONObject json) { +// private ResponseModel handleSendMessage(JSONObject json) { +// +// try { +// if (currentUser == null) +// return new ResponseModel("error", "Unauthorized. Please login first."); +// +// UUID messageId = UUID.randomUUID(); +// UUID senderId = currentUser.getInternal_uuid(); +// String receiverType = json.getString("receiver_type"); +// UUID receiverId; +// receiverId = UUID.fromString(json.getString("receiver_id")); +// +// 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)."); +// } +// } +// +// +// String content = json.optString("content", ""); +// String messageType = json.optString("message_type", "TEXT"); +// +// boolean inserted = MessageDatabase.insertMessage(messageId, senderId, receiverId, receiverType, content, messageType); +// if (!inserted) +// return new ResponseModel("error", "Failed to insert message."); +// +// if (json.has("attachments")) { +// JSONArray attachmentsArray = json.getJSONArray("attachments"); +// List attachments = new ArrayList<>(); +// +// for (int i = 0; i < attachmentsArray.length(); i++) { +// JSONObject attJson = attachmentsArray.getJSONObject(i); +// attachments.add(new FileAttachment( +// attJson.getString("file_url"), +// attJson.getString("file_type") +// )); +// } +// +// boolean attInserted = MessageDatabase.insertAttachments(messageId, attachments); +// if (!attInserted) +// return new ResponseModel("error", "Message inserted but failed to attach files."); +// } +// +// // Send real-time message +// Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now()); +// List receivers = getReceiversForChat(receiverId, receiverType); +// receivers.remove(senderId); +// RealTimeEventDispatcher.sendNewMessage(msg, receivers); +// +// // Update chat list (last_message_time) +// JSONObject chatUpdate = new JSONObject(); +// chatUpdate.put("chat_id", receiverId.toString()); +// chatUpdate.put("chat_type", receiverType); +// chatUpdate.put("last_message_time", LocalDateTime.now().toString()); +// +// JSONObject chatPayload = new JSONObject(); +// chatPayload.put("action", "chat_updated"); +// chatPayload.put("data", chatUpdate); +// +// for (UUID receiver : receivers) { +// RealTimeEventDispatcher.sendToUser(receiver, chatPayload); +// } +// +// JSONObject data = new JSONObject(); +// data.put("message_id", messageId.toString()); +// return new ResponseModel("success", "Message sent successfully.", data); +// +// } catch (Exception e) { +// e.printStackTrace(); +// return new ResponseModel("error", "Exception occurred while sending message."); +// } +// } + + + private ResponseModel handleSendMessage(JSONObject json) { try { if (currentUser == null) return new ResponseModel("error", "Unauthorized. Please login first."); @@ -2478,68 +2560,124 @@ public class ClientHandler implements Runnable { UUID messageId = UUID.randomUUID(); UUID senderId = currentUser.getInternal_uuid(); String receiverType = json.getString("receiver_type"); - UUID receiverId; - receiverId = UUID.fromString(json.getString("receiver_id")); - - if(Objects.equals(receiverType, "private")){ - PrivateChatDatabase.clearDeletedFlag(senderId, receiverId); - 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)."); - } - } + UUID receiverId = UUID.fromString(json.getString("receiver_id")); + // private validations... + // ... String content = json.optString("content", ""); String messageType = json.optString("message_type", "TEXT"); - boolean inserted = MessageDatabase.insertMessage(messageId, senderId, receiverId, receiverType, content, messageType); - if (!inserted) - return new ResponseModel("error", "Failed to insert message."); - + // Parse attachments + List attachments = new ArrayList<>(); if (json.has("attachments")) { - JSONArray attachmentsArray = json.getJSONArray("attachments"); - List attachments = new ArrayList<>(); - - for (int i = 0; i < attachmentsArray.length(); i++) { - JSONObject attJson = attachmentsArray.getJSONObject(i); + JSONArray arr = json.getJSONArray("attachments"); + for (int i = 0; i < arr.length(); i++) { + JSONObject a = arr.getJSONObject(i); attachments.add(new FileAttachment( - attJson.getString("file_url"), - attJson.getString("file_type") + a.optString("file_url",""), + a.optString("file_type","FILE"), + a.optString("file_name",""), + a.has("file_size") && !a.isNull("file_size") ? a.getLong("file_size") : null, + a.optString("mime_type", null), + a.has("width") && !a.isNull("width") ? a.getInt("width") : null, + a.has("height") && !a.isNull("height") ? a.getInt("height") : null, + a.has("duration_seconds") && !a.isNull("duration_seconds") ? a.getInt("duration_seconds") : null, + a.isNull("thumbnail_url") ? null : a.optString("thumbnail_url", null) )); } - - boolean attInserted = MessageDatabase.insertAttachments(messageId, attachments); - if (!attInserted) - return new ResponseModel("error", "Message inserted but failed to attach files."); } - // Send real-time message + if ((content == null || content.isBlank()) && attachments.isEmpty()) { + return new ResponseModel("error", "Empty message: no content or attachment."); + } + + // Harmonize message_type + if (!attachments.isEmpty()) { + String firstType = attachments.get(0).getFileType(); + if ("TEXT".equalsIgnoreCase(messageType)) { + messageType = firstType; + } else if (!messageType.equalsIgnoreCase(firstType) && !messageType.equalsIgnoreCase("FILE")) { + return new ResponseModel("error", "message_type and attachment.file_type mismatch."); + } + } + + // DB transaction + try (Connection conn = ConnectionDb.connect()) { + conn.setAutoCommit(false); + + boolean inserted = MessageDatabase.insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType); + if (!inserted) { + conn.rollback(); + return new ResponseModel("error", "Failed to insert message."); + } + + if (!attachments.isEmpty()) { + boolean attInserted = MessageDatabase.insertAttachmentsTx(conn, messageId, attachments); + if (!attInserted) { + conn.rollback(); + return new ResponseModel("error", "Message inserted but failed to attach files."); + } + } + + conn.commit(); + } + + // Real-Time Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now()); + + // رویداد با پیوست‌ها + JSONObject payload = new JSONObject(); + payload.put("action", "new_message"); + JSONObject data = new JSONObject(); + data.put("id", messageId.toString()); + data.put("sender_id", senderId.toString()); + data.put("receiver_id", receiverId.toString()); + data.put("receiver_type", receiverType); + data.put("content", content); + data.put("message_type", messageType); + data.put("send_at", msg.getSend_at().toString()); + + if (!attachments.isEmpty()) { + JSONArray out = new JSONArray(); + for (FileAttachment a : attachments) { + JSONObject ao = new JSONObject() + .put("file_url", a.getFileUrl()) + .put("file_type", a.getFileType()) + .put("file_name", a.getFileName() == null ? JSONObject.NULL : a.getFileName()) + .put("file_size", a.getFileSize() == null ? JSONObject.NULL : a.getFileSize()) + .put("mime_type", a.getMimeType() == null ? JSONObject.NULL : a.getMimeType()) + .put("width", a.getWidth() == null ? JSONObject.NULL : a.getWidth()) + .put("height", a.getHeight() == null ? JSONObject.NULL : a.getHeight()) + .put("duration_seconds", a.getDurationSeconds() == null ? JSONObject.NULL : a.getDurationSeconds()) + .put("thumbnail_url", a.getThumbnailUrl() == null ? JSONObject.NULL : a.getThumbnailUrl()); + out.put(ao); + } + data.put("attachments", out); + } + + User sender = userDatabase.findByInternalUUID(senderId); + if (sender != null) data.put("sender_name", sender.getProfile_name()); + payload.put("data", data); + List receivers = getReceiversForChat(receiverId, receiverType); receivers.remove(senderId); - RealTimeEventDispatcher.sendNewMessage(msg, receivers); + RealTimeEventDispatcher.broadcastToUsers(receivers, payload); - // Update chat list (last_message_time) - JSONObject chatUpdate = new JSONObject(); - chatUpdate.put("chat_id", receiverId.toString()); - chatUpdate.put("chat_type", receiverType); - chatUpdate.put("last_message_time", LocalDateTime.now().toString()); + // chat_updated + JSONObject chatUpdate = new JSONObject() + .put("chat_id", receiverId.toString()) + .put("chat_type", receiverType) + .put("last_message_time", LocalDateTime.now().toString()); - JSONObject chatPayload = new JSONObject(); - chatPayload.put("action", "chat_updated"); - chatPayload.put("data", chatUpdate); + JSONObject chatPayload = new JSONObject() + .put("action", "chat_updated") + .put("data", chatUpdate); - for (UUID receiver : receivers) { - RealTimeEventDispatcher.sendToUser(receiver, chatPayload); - } + for (UUID r : receivers) RealTimeEventDispatcher.sendToUser(r, chatPayload); - JSONObject data = new JSONObject(); - data.put("message_id", messageId.toString()); - return new ResponseModel("success", "Message sent successfully.", data); + JSONObject respData = new JSONObject().put("message_id", messageId.toString()); + return new ResponseModel("success", "Message sent successfully.", respData); } catch (Exception e) { e.printStackTrace(); @@ -2548,6 +2686,7 @@ public class ClientHandler implements Runnable { } + private List getReceiversForChat(UUID receiverId, String receiverType) { switch (receiverType) { case "private": diff --git a/src/main/java/org/to/telegramfinalproject/Server/MainServer.java b/src/main/java/org/to/telegramfinalproject/Server/MainServer.java index 2318fa3..abf1de0 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/MainServer.java +++ b/src/main/java/org/to/telegramfinalproject/Server/MainServer.java @@ -9,6 +9,7 @@ import java.net.Socket; public class MainServer { private static final int PORT = 8000; + public static void main(String[] args) { try (ServerSocket serverSocket = new ServerSocket(PORT)) { System.out.println("Server started on port " + PORT); diff --git a/src/main/java/org/to/telegramfinalproject/Server/TestServer.java b/src/main/java/org/to/telegramfinalproject/Server/TestServer.java new file mode 100644 index 0000000..ddbf0c3 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/TestServer.java @@ -0,0 +1,57 @@ +package org.to.telegramfinalproject.Server; + +import org.to.telegramfinalproject.Database.userDatabase; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class TestServer { + private static final int SOCKET_PORT = 8000; // سرور سوکت + private static final int HTTP_PORT = 8080; // سرور آپلود + private static final String UPLOAD_BASE_DIR = "uploads"; // پوشه‌ی ذخیره فایل‌ها + + public static void main(String[] args) { + // 1) استارت HTTP Upload در ترد جدا + Thread httpThread = new Thread(() -> { + try { + UploadHttp.start(HTTP_PORT, UPLOAD_BASE_DIR); + } catch (IOException e) { + System.err.println("Upload HTTP failed to start: " + e.getMessage()); + e.printStackTrace(); + } + }, "upload-http"); + httpThread.setDaemon(true); + httpThread.start(); + + // 2) سرور سوکت با Thread Pool + ExecutorService pool = Executors.newCachedThreadPool(); + try (ServerSocket serverSocket = new ServerSocket(SOCKET_PORT)) { + System.out.println("Socket server started on port " + SOCKET_PORT); + userDatabase.setAllUsersOffline(); + + // 3) Shutdown Hook برای خاموشی تمیز + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("\nShutting down..."); + try { serverSocket.close(); } catch (IOException ignore) {} + pool.shutdownNow(); + userDatabase.setAllUsersOffline(); + System.out.println("Goodbye."); + })); + + // 4) حلقه پذیرش اتصال‌ها + while (!serverSocket.isClosed()) { + Socket clientSocket = serverSocket.accept(); + clientSocket.setTcpNoDelay(true); + System.out.println("New client connected: " + clientSocket.getInetAddress()); + pool.submit(new ClientHandler(clientSocket)); + } + + } catch (IOException e) { + System.err.println("Socket server error: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java b/src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java index 8a99837..fd1e77a 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java +++ b/src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java @@ -1,35 +1,20 @@ package org.to.telegramfinalproject.Server; import static spark.Spark.*; -import javax.servlet.MultipartConfigElement; -import javax.servlet.http.Part; -import java.io.IOException; -import java.nio.file.*; -import java.io.InputStream; - -import org.json.JSONObject; - import javax.imageio.ImageIO; +import javax.servlet.MultipartConfigElement; +import javax.servlet.http.Part; + import java.awt.image.BufferedImage; -import java.nio.file.*; - - - - +import java.io.InputStream; +import java.io.IOException; import java.nio.file.*; import java.time.LocalDate; +import javax.sound.sampled.*; // برای WAV -// برای ویدیو (MP4 و …) -import org.jcodec.api.FrameGrab; -import org.jcodec.common.io.NIOUtils; -import org.jcodec.common.model.Picture; -import org.jcodec.scale.AWTUtil; -//import org.jcodec.containers.mp4.MP4Demuxer; -//import org.jcodec.containers.mp4.MP4DemuxerTrack; - -// برای MP3 +import org.json.JSONObject; import com.mpatric.mp3agic.Mp3File; public class UploadHttp { @@ -67,7 +52,7 @@ public class UploadHttp { String original = filePart.getSubmittedFileName(); String ext = guessExt(original, mime); String day = LocalDate.now().toString(); - String typeDir = subdirFor(mime); // images/videos/audios/files + String typeDir = subdirFor(mime); // images/audios/files String subdir = typeDir + "/" + day; String name = java.util.UUID.randomUUID() + ext; @@ -84,39 +69,15 @@ public class UploadHttp { String fileUrl = "/" + subdir.replace('\\', '/') + "/" + name; String fileType = mapToFileType(mime); - // متادیتا + //Meta deta only for audio and image Integer width = null, height = null, durationSeconds = null; String thumbnailUrl = null; if ("IMAGE".equals(fileType) || "GIF".equals(fileType)) { int[] wh = imageSize(target); if (wh != null) { width = wh[0]; height = wh[1]; } - } else if ("VIDEO".equals(fileType)) { - // تلاش برای استخراج width/height/duration با JCodec - VideoMeta vm = videoMeta(target); - if (vm != null) { - width = vm.width; - height = vm.height; - durationSeconds = vm.durationSeconds; - } - // ساخت thumbnail (اختیاری) - try { - String thumbName = name.replace(ext, "") + "_thumb.jpg"; - Path thumbDir = basePath.resolve("thumbs/" + day).normalize(); - Files.createDirectories(thumbDir); - Path thumbTarget = thumbDir.resolve(thumbName).normalize(); - if (makeVideoThumbnail(target, thumbTarget)) { - thumbnailUrl = "/thumbs/" + day + "/" + thumbName; - } - } catch (Exception ignore) {} } else if ("AUDIO".equals(fileType)) { - // اگر MP3 بود، مدت را با mp3agic بگیر - if ("audio/mpeg".equalsIgnoreCase(mime) || ext.equalsIgnoreCase(".mp3")) { - try { - Mp3File mp3 = new Mp3File(target.toFile()); - durationSeconds = (int) mp3.getLengthInSeconds(); - } catch (Exception ignore) {} - } + durationSeconds = audioDurationSeconds(target, mime, ext); } res.status(200); @@ -129,11 +90,11 @@ public class UploadHttp { .put("width", width == null ? JSONObject.NULL : width) .put("height", height == null ? JSONObject.NULL : height) .put("duration_seconds", durationSeconds == null ? JSONObject.NULL : durationSeconds) - .put("thumbnail_url", thumbnailUrl == null ? JSONObject.NULL : thumbnailUrl) + .put("thumbnail_url", JSONObject.NULL) .toString(); } catch (Exception e) { - e.printStackTrace(); // لوکال + e.printStackTrace(); res.status(500); return jsonError("internal error"); } @@ -153,7 +114,6 @@ public class UploadHttp { private static String subdirFor(String mime) { String m = mime.toLowerCase(); if (m.startsWith("image/")) return "images"; - if (m.startsWith("video/")) return "videos"; if (m.startsWith("audio/")) return "audios"; return "files"; } @@ -164,7 +124,6 @@ public class UploadHttp { if (m.contains("gif")) return "GIF"; return "IMAGE"; } - if (m.startsWith("video/")) return "VIDEO"; if (m.startsWith("audio/")) return "AUDIO"; return "FILE"; } @@ -177,14 +136,13 @@ public class UploadHttp { if ("image/png".equalsIgnoreCase(mime)) return ".png"; if ("image/jpeg".equalsIgnoreCase(mime)) return ".jpg"; if ("image/gif".equalsIgnoreCase(mime)) return ".gif"; - if ("video/mp4".equalsIgnoreCase(mime)) return ".mp4"; if ("audio/mpeg".equalsIgnoreCase(mime)) return ".mp3"; + if ("audio/wav".equalsIgnoreCase(mime) || "audio/x-wav".equalsIgnoreCase(mime)) return ".wav"; if ("application/pdf".equalsIgnoreCase(mime)) return ".pdf"; return ""; } private static String safeName(String name) { - // پاک‌سازی خیلی ساده برای خروجی return name.replace("\"", "").replace("\n", "").replace("\r", ""); } @@ -196,49 +154,30 @@ public class UploadHttp { return null; } - // --- Video meta via JCodec --- - private static class VideoMeta { - final Integer width, height, durationSeconds; - VideoMeta(Integer w, Integer h, Integer d) { this.width = w; this.height = h; this.durationSeconds = d; } - } - - private static VideoMeta videoMeta(Path file) { + //only audio + private static Integer audioDurationSeconds(Path file, String mime, String ext) { try { - // Width/Height از طریق اولین فریم - BufferedImage first = null; - try { - FrameGrab grab = FrameGrab.createFrameGrab(NIOUtils.readableChannel(file.toFile())); - Picture p = grab.getNativeFrame(); - if (p != null) first = AWTUtil.toBufferedImage(p); - } catch (Exception ignore) {} + if ("audio/mpeg".equalsIgnoreCase(mime) || ".mp3".equalsIgnoreCase(ext)) { + Mp3File mp3 = new Mp3File(file.toFile()); + return (int) mp3.getLengthInSeconds(); + } - Integer w = null, h = null; - if (first != null) { w = first.getWidth(); h = first.getHeight(); } - - // Duration از Demuxer (فقط MP4ها عالی جواب میده) - Integer dur = null; -// try { -// MP4Demuxer demuxer = new MP4Demuxer(NIOUtils.readableChannel(file.toFile())); -// MP4DemuxerTrack vt = (MP4DemuxerTrack) demuxer.getVideoTrack(); -// double seconds = vt.getMeta().getTotalDuration(); -// dur = (int) Math.round(seconds); -// } catch (Exception ignore) {} - - if (w != null || h != null || dur != null) return new VideoMeta(w, h, dur); - } catch (Exception ignore) {} + // WAV با javax.sound.sampled + if ("audio/wav".equalsIgnoreCase(mime) || "audio/x-wav".equalsIgnoreCase(mime) || ".wav".equalsIgnoreCase(ext)) { + try (AudioInputStream ais = AudioSystem.getAudioInputStream(file.toFile())) { + AudioFormat format = ais.getFormat(); + long frames = ais.getFrameLength(); + if (frames > 0 && format.getFrameRate() > 0) { + double seconds = frames / format.getFrameRate(); + return (int)Math.round(seconds); + } + } + } + } catch (UnsupportedAudioFileException | IOException ignore) { + // فرمت صوتی پشتیبانی نشده برای AudioSystem + } catch (Exception ignore) { + // mp3agic یا سایر استثناها + } return null; } - - private static boolean makeVideoThumbnail(Path videoFile, Path thumbTarget) { - try { - FrameGrab grab = FrameGrab.createFrameGrab(NIOUtils.readableChannel(videoFile.toFile())); - Picture p = grab.getNativeFrame(); - if (p == null) return false; - BufferedImage bi = AWTUtil.toBufferedImage(p); - Files.createDirectories(thumbTarget.getParent()); - return ImageIO.write(bi, "jpg", thumbTarget.toFile()); - } catch (Exception e) { - return false; - } - } }