From ce4434650d2cf7ba56a37737632bdd8108bd72bb Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Tue, 12 Aug 2025 14:09:57 +0330 Subject: [PATCH] work on send media --- build.gradle | 1 + .../Client/ActionHandler.java | 272 +++++++++--------- .../Client/MediaSender.java | 82 ++++++ .../Server/ClientHandler.java | 207 ++++++++++++- 4 files changed, 423 insertions(+), 139 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Client/MediaSender.java diff --git a/build.gradle b/build.gradle index 2c9cfb2..5707d6c 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,7 @@ dependencies { exclude group: 'org.openjfx' } //For upload files + implementation 'org.slf4j:slf4j-simple:2.0.13' implementation 'com.sparkjava:spark-core:2.9.4' implementation 'com.mpatric:mp3agic:0.9.1' //for mp3 implementation 'org.json:json:20231013' diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 604c393..a78e3f5 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -3345,69 +3345,69 @@ public class ActionHandler { } -// public void sendMessage(UUID receiverId, String receiverType) { -// Scanner scanner = new Scanner(System.in); -// -// System.out.print("Enter your message: "); -// String content = scanner.nextLine(); -// -// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); -// String messageType = scanner.nextLine(); -// Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); -// while (!allowedTypes.contains(messageType.toUpperCase())) { -// System.out.println("❌ Invalid message type. Try again (TEXT / IMAGE / VIDEO / FILE): "); -// messageType = scanner.nextLine(); -// } -// messageType = messageType.toUpperCase(); -// -// JSONArray attachmentsArray = new JSONArray(); -// -// System.out.print("Do you want to attach files? (yes/no): "); -// if (scanner.nextLine().equalsIgnoreCase("yes")) { -// while (true) { -// System.out.print("File URL: "); -// String fileUrl = scanner.nextLine(); -// -// System.out.print("File Type (IMAGE / VIDEO / FILE): "); -// String fileType = scanner.nextLine(); -// -// JSONObject fileJson = new JSONObject(); -// fileJson.put("file_url", fileUrl); -// fileJson.put("file_type", fileType); -// attachmentsArray.put(fileJson); -// -// System.out.print("Add another file? (yes/no): "); -// if (!scanner.nextLine().equalsIgnoreCase("yes")) { -// break; -// } -// } -// } -// -// -// -// JSONObject messageJson = new JSONObject(); -// messageJson.put("action", "send_message"); -// messageJson.put("receiver_type", receiverType); -// messageJson.put("content", content); -// messageJson.put("message_type", messageType); -// if (receiverType.equals("private")) { -// messageJson.put("receiver_user_id", receiverId.toString()); -// } else { -// messageJson.put("receiver_id", receiverId.toString()); -// } -// -// if (!attachmentsArray.isEmpty()) { -// messageJson.put("attachments", attachmentsArray); -// } -// -// JSONObject response = sendWithResponse(messageJson); -// if (response != null && response.getString("status").equals("success")) { -// System.out.println("✅ Message sent successfully! ID: " + response.getJSONObject("data").getString("message_id")); -// } else { -// System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "no response")); -// } -// -// } + public void sendMessage(UUID receiverId, String receiverType) { + Scanner scanner = new Scanner(System.in); + + System.out.print("Enter your message: "); + String content = scanner.nextLine(); + + System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): "); + String messageType = scanner.nextLine(); + Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); + while (!allowedTypes.contains(messageType.toUpperCase())) { + System.out.println("❌ Invalid message type. Try again (TEXT / IMAGE / VIDEO / FILE): "); + messageType = scanner.nextLine(); + } + messageType = messageType.toUpperCase(); + + JSONArray attachmentsArray = new JSONArray(); + + System.out.print("Do you want to attach files? (yes/no): "); + if (scanner.nextLine().equalsIgnoreCase("yes")) { + while (true) { + System.out.print("File URL: "); + String fileUrl = scanner.nextLine(); + + System.out.print("File Type (IMAGE / VIDEO / FILE): "); + String fileType = scanner.nextLine(); + + JSONObject fileJson = new JSONObject(); + fileJson.put("file_url", fileUrl); + fileJson.put("file_type", fileType); + attachmentsArray.put(fileJson); + + System.out.print("Add another file? (yes/no): "); + if (!scanner.nextLine().equalsIgnoreCase("yes")) { + break; + } + } + } + + + + JSONObject messageJson = new JSONObject(); + messageJson.put("action", "send_message"); + messageJson.put("receiver_type", receiverType); + messageJson.put("content", content); + messageJson.put("message_type", messageType); + if (receiverType.equals("private")) { + messageJson.put("receiver_user_id", receiverId.toString()); + } else { + messageJson.put("receiver_id", receiverId.toString()); + } + + if (!attachmentsArray.isEmpty()) { + messageJson.put("attachments", attachmentsArray); + } + + JSONObject response = sendWithResponse(messageJson); + if (response != null && response.getString("status").equals("success")) { + System.out.println("✅ Message sent successfully! ID: " + response.getJSONObject("data").getString("message_id")); + } else { + System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "no response")); + } + + } @@ -3465,79 +3465,79 @@ public class ActionHandler { // } - public void sendMessage(UUID chatId, String receiverType) { - Scanner scanner = new Scanner(System.in); - - System.out.print("Enter your message (leave empty if file only): "); - String content = scanner.nextLine(); - - System.out.print("Enter message type (TEXT / IMAGE / AUDIO / FILE / GIF): "); - String messageType = scanner.nextLine().toUpperCase(); - Set allowedTypes = Set.of("TEXT", "IMAGE", "AUDIO", "FILE", "GIF"); - while (!allowedTypes.contains(messageType)) { - System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / AUDIO / FILE / GIF): "); - messageType = scanner.nextLine().toUpperCase(); - } - - JSONArray attachmentsArray = new JSONArray(); - System.out.print("Attach files? (yes/no): "); - if (scanner.nextLine().equalsIgnoreCase("yes")) { - while (true) { - 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); - } - - attachmentsArray.put(fileJson); - - System.out.print("Add another file? (yes/no): "); - if (!scanner.nextLine().equalsIgnoreCase("yes")) break; - } - } - - JSONObject messageJson = new JSONObject(); - messageJson.put("action", "send_message"); - messageJson.put("receiver_type", receiverType); // "private"/"group"/"channel" - messageJson.put("receiver_id", chatId.toString()); // در private = chat_id - messageJson.put("content", content); - messageJson.put("message_type", messageType); - if (attachmentsArray.length() > 0) { - 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.optString("message","No message") : "No response")); - } - } - +// public void sendMessage(UUID chatId, String receiverType) { +// Scanner scanner = new Scanner(System.in); +// +// System.out.print("Enter your message (leave empty if file only): "); +// String content = scanner.nextLine(); +// +// System.out.print("Enter message type (TEXT / IMAGE / AUDIO / FILE / GIF): "); +// String messageType = scanner.nextLine().toUpperCase(); +// Set allowedTypes = Set.of("TEXT", "IMAGE", "AUDIO", "FILE", "GIF"); +// while (!allowedTypes.contains(messageType)) { +// System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / AUDIO / FILE / GIF): "); +// messageType = scanner.nextLine().toUpperCase(); +// } +// +// JSONArray attachmentsArray = new JSONArray(); +// System.out.print("Attach files? (yes/no): "); +// if (scanner.nextLine().equalsIgnoreCase("yes")) { +// while (true) { +// 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); +// } +// +// attachmentsArray.put(fileJson); +// +// System.out.print("Add another file? (yes/no): "); +// if (!scanner.nextLine().equalsIgnoreCase("yes")) break; +// } +// } +// +// JSONObject messageJson = new JSONObject(); +// messageJson.put("action", "send_message"); +// messageJson.put("receiver_type", receiverType); // "private"/"group"/"channel" +// messageJson.put("receiver_id", chatId.toString()); // در private = chat_id +// messageJson.put("content", content); +// messageJson.put("message_type", messageType); +// if (attachmentsArray.length() > 0) { +// 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.optString("message","No message") : "No response")); +// } +// } +// private void refreshContactList() { JSONObject req = new JSONObject(); req.put("action", "get_contact_list"); diff --git a/src/main/java/org/to/telegramfinalproject/Client/MediaSender.java b/src/main/java/org/to/telegramfinalproject/Client/MediaSender.java new file mode 100644 index 0000000..3eec675 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/MediaSender.java @@ -0,0 +1,82 @@ +package org.to.telegramfinalproject.Client; + +import org.json.JSONObject; +import java.io.*; +import java.net.Socket; +import java.nio.file.Files; +import java.util.UUID; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; + + + +public class MediaSender { + + public static void sendImageOrAudio(Socket socket, + UUID senderId, + String receiverType, + UUID receiverId, + File file, + String messageType, // "IMAGE" یا "AUDIO" + String captionOrEmpty) throws Exception { + + if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType)) + throw new IllegalArgumentException("Only IMAGE/AUDIO"); + + // 1) اعلام سوییچ به باینری + PrintWriter textOut = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true); + textOut.println("MEDIA"); + + // 2) متادیتا + String mime = Files.probeContentType(file.toPath()); + if (mime == null) mime = "application/octet-stream"; + + Integer width = null, height = null; + if ("IMAGE".equals(messageType)) { + try { + BufferedImage img = ImageIO.read(file); + if (img != null) { width = img.getWidth(); height = img.getHeight(); } + } catch (Exception ignore) {} + } + + UUID messageId = UUID.randomUUID(); + JSONObject header = new JSONObject() + .put("message_id", messageId.toString()) + .put("sender_id", senderId.toString()) + .put("receiver_type", receiverType) + .put("receiver_id", receiverId.toString()) + .put("message_type", messageType) + .put("file_name", file.getName()) + .put("mime_type", mime) + .put("file_size", file.length()) + .put("text", captionOrEmpty == null ? "" : captionOrEmpty); + + if (width != null) header.put("width", width); + if (height != null) header.put("height", height); + + byte[] headerBytes = header.toString().getBytes("UTF-8"); + + // 3) ارسال فریم باینری + DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); + dos.writeInt(0x4D444D31); // MAGIC + dos.writeInt(headerBytes.length); // headerLen + dos.write(headerBytes); // header + dos.writeLong(file.length()); // contentLen + + try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { + byte[] buf = new byte[8192]; + int n; + while ((n = fis.read(buf)) != -1) { + dos.write(buf, 0, n); + } + } + dos.flush(); + + // (اختیاری) Ack متنی + BufferedReader textIn = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); + String ack = textIn.readLine(); + if (!"OK".equalsIgnoreCase(ack)) { + throw new IOException("Server did not ACK: " + ack); + } + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 121772f..bdad647 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -29,11 +29,24 @@ public class ClientHandler implements Runnable { UUID userId = null; try ( - BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - PrintWriter out = new PrintWriter(socket.getOutputStream(), true) +// BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// PrintWriter out = new PrintWriter(socket.getOutputStream(), true) + BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); + DataInputStream dis = new DataInputStream(bis); // برای MEDIA و هدرهای باینری + + PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8), true); + ) { + +// DataInputStream bin = new DataInputStream(new BufferedInputStream(socket.getInputStream())); + String inputLine; - while ((inputLine = in.readLine()) != null) { + while ((inputLine = readUtf8Line(bis)) != null) { + if ("MEDIA".equalsIgnoreCase(inputLine.trim())) { + handleMediaFrame(dis, out); + continue; + } + JSONObject requestJson = new JSONObject(inputLine); String action = requestJson.getString("action"); ResponseModel response = null; @@ -2579,6 +2592,194 @@ public class ClientHandler implements Runnable { } + private static String readUtf8Line(BufferedInputStream bis) throws java.io.IOException { + StringBuilder sb = new StringBuilder(); + while (true) { + int b = bis.read(); + if (b == -1) { + return sb.length() == 0 ? null : sb.toString(); + } + if (b == '\n') { + int len = sb.length(); + if (len > 0 && sb.charAt(len - 1) == '\r') sb.setLength(len - 1); + return sb.toString(); + } + sb.append((char) b); // برای کنترل‌لاین‌های ASCII/UTF-8 OK + } + } + + + private void handleMediaFrame(DataInputStream dis, PrintWriter out) { + try { + // MAGIC = "MDM1" + final int MAGIC_EXPECTED = 0x4D444D31; + int magic = dis.readInt(); + if (magic != MAGIC_EXPECTED) { + out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); + out.flush(); + return; + } + + int headerLen = dis.readInt(); + if (headerLen <= 0 || headerLen > (64 * 1024)) { + out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); + out.flush(); + return; + } + + byte[] headerBytes = dis.readNBytes(headerLen); + if (headerBytes.length != headerLen) { + out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); + out.flush(); + return; + } + JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8)); + + long contentLen = dis.readLong(); + long MAX_MEDIA = 25L * 1024 * 1024; + if (contentLen <= 0 || contentLen > MAX_MEDIA) { + skip(dis, contentLen); + out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); + out.flush(); + return; + } + + // الزامی‌ها + UUID messageId = UUID.fromString(h.getString("message_id")); + UUID senderId = UUID.fromString(h.getString("sender_id")); + String rType = h.getString("receiver_type"); // private/group/channel + UUID receiverId = UUID.fromString(h.getString("receiver_id")); + String messageType = h.getString("message_type"); // IMAGE | AUDIO + + if (!"IMAGE".equalsIgnoreCase(messageType) && !"AUDIO".equalsIgnoreCase(messageType)) { + skip(dis, contentLen); + out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); + out.flush(); + return; + } + + String fileName = h.optString("file_name", "file.bin"); + String mimeType = h.optString("mime_type", "application/octet-stream"); + String text = h.optString("text", ""); + + Integer width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null; + Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null; + + if (fileName.length() > 200) fileName = fileName.substring(0, 200); + + // مسیر ذخیره + java.nio.file.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize(); + java.nio.file.Files.createDirectories(baseDir); + String kind = "IMAGE".equalsIgnoreCase(messageType) ? "images" : "audios"; + String subdir = kind + "/" + java.time.LocalDate.now(); + java.nio.file.Path dir = baseDir.resolve(subdir).normalize(); + java.nio.file.Files.createDirectories(dir); + + String ext = guessExt(fileName, mimeType); + String storedName = java.util.UUID.randomUUID() + ext; + java.nio.file.Path target = dir.resolve(storedName).normalize(); + + // دریافت بایت‌های فایل + try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream( + target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) { + long remaining = contentLen; + byte[] buf = new byte[8192]; + while (remaining > 0) { + int toRead = (int) Math.min(buf.length, remaining); + int n = dis.read(buf, 0, toRead); + if (n == -1) throw new EOFException("stream ended early"); + fos.write(buf, 0, n); + remaining -= n; + } + } + + long fileSize = java.nio.file.Files.size(target); + String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; + + FileAttachment att = new FileAttachment( + fileUrl, + messageType.toUpperCase(), // IMAGE/AUDIO + fileName, + fileSize, + mimeType, + width, + height, + null, // durationSeconds + null // thumbnailUrl + ); + + boolean ok = MessageDatabase.saveMessageWithOptionalAttachments( + messageId, senderId, receiverId, rType, text, messageType.toUpperCase(), java.util.List.of(att) + ); + + JSONObject ack = new JSONObject() + .put("status", ok ? "success" : "error") + .put("message_id", messageId.toString()) + .put("file_url", fileUrl) + .put("file_size", fileSize) + .put("mime_type", mimeType); + + out.println(ack.toString()); + out.flush(); + + } catch (Exception e) { + e.printStackTrace(); + out.println(new JSONObject().put("status","error").put("message","exception").toString()); + out.flush(); + } + } + + private static void skip(DataInputStream dis, long n) throws IOException { + if (n <= 0) return; + byte[] buf = new byte[8192]; + long left = n; + while (left > 0) { + int toRead = (int) Math.min(buf.length, left); + int r = dis.read(buf, 0, toRead); + if (r == -1) break; // EOF + left -= r; + } + } + + private static String guessExt(String original, String mime) { + // اول از نام اصلی (امن چون فقط برای اکستنشن استفاده می‌کنی) + if (original != null && original.contains(".")) { + String ext = original.substring(original.lastIndexOf('.')); + if (ext.length() <= 10) return ext.toLowerCase(); + } + if (mime == null) return ""; + + String m = mime.toLowerCase(); + + // تصاویر + if (m.equals("image/png")) return ".png"; + if (m.equals("image/jpeg") || m.equals("image/jpg")) return ".jpg"; + if (m.equals("image/gif")) return ".gif"; + if (m.equals("image/webp")) return ".webp"; + + // صوت + if (m.equals("audio/mpeg") || m.equals("audio/mp3")) return ".mp3"; + if (m.equals("audio/ogg")) return ".ogg"; + if (m.equals("audio/opus")) return ".opus"; + if (m.equals("audio/wav") || m.equals("audio/x-wav")) return ".wav"; + if (m.equals("audio/m4a") || m.equals("audio/mp4")) return ".m4a"; + + // ویدیو (اگر بعدا اضافه شد) + if (m.equals("video/mp4")) return ".mp4"; + if (m.equals("video/webm")) return ".webm"; + + // fallback + if (m.startsWith("image/")) return ""; // بگذار بدون اکستنشن ذخیره شود + if (m.startsWith("audio/")) return ""; + if (m.startsWith("video/")) return ""; + + return ""; + } + + + + + // private ResponseModel handleSendMessage(JSONObject json) { //