From 17366f5ea64fd67daf0dfffb3d8437c8a2bfa5a0 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Sun, 10 Aug 2025 12:46:05 +0330 Subject: [PATCH 01/16] work on send file --- build.gradle | 6 + src/main/java/module-info.java | 6 + .../Database/MessageDatabase.java | 178 ++++++++++--- .../Models/FileAttachment.java | 62 ++++- .../Server/UploadHttp.java | 244 ++++++++++++++++++ 5 files changed, 451 insertions(+), 45 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java diff --git a/build.gradle b/build.gradle index 96bc6d1..d17a5d0 100644 --- a/build.gradle +++ b/build.gradle @@ -44,11 +44,17 @@ dependencies { implementation('net.synedra:validatorfx:0.5.0') { exclude group: 'org.openjfx' } + //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' implementation 'org.kordamp.bootstrapfx:bootstrapfx-core:0.4.0' implementation('eu.hansolo:tilesfx:21.0.3') { exclude group: 'org.openjfx' + } test { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 0024ec0..2645769 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -11,6 +11,12 @@ 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 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/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index db997ee..35e644d 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -66,6 +66,87 @@ public class MessageDatabase { } } + + public static boolean insertMessageTx(Connection conn, UUID messageId, UUID senderId, UUID receiverId, + String receiverType, String content, String messageType) throws SQLException { + String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setObject(1, messageId); + ps.setObject(2, senderId); + ps.setString(3, receiverType); + ps.setObject(4, receiverId); + if (content == null || content.isBlank()) ps.setNull(5, java.sql.Types.VARCHAR); else ps.setString(5, content); + ps.setString(6, messageType); + return ps.executeUpdate() > 0; + } + } + + public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List attachments) throws SQLException { + if (attachments == null || attachments.isEmpty()) return true; + String sql = """ + INSERT INTO message_attachments + (attachment_id, message_id, file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; + try (PreparedStatement ps = conn.prepareStatement(sql)) { + for (FileAttachment att : attachments) { + ps.setObject(1, UUID.randomUUID()); + ps.setObject(2, messageId); + ps.setString(3, att.getFileUrl()); + ps.setString(4, att.getFileType()); // IMAGE/VIDEO/AUDIO/FILE/GIF/STICKER + ps.setString(5, att.getFileName()); + if (att.getFileSize() != null) ps.setLong(6, att.getFileSize()); else ps.setNull(6, java.sql.Types.BIGINT); + ps.setString(7, att.getMimeType()); + if (att.getWidth() != null) ps.setInt(8, att.getWidth()); else ps.setNull(8, java.sql.Types.INTEGER); + if (att.getHeight() != null) ps.setInt(9, att.getHeight()); else ps.setNull(9, java.sql.Types.INTEGER); + if (att.getDurationSeconds() != null) ps.setInt(10, att.getDurationSeconds()); else ps.setNull(10, java.sql.Types.INTEGER); + ps.setString(11, att.getThumbnailUrl()); + ps.addBatch(); + } + ps.executeBatch(); + return true; + } + } + + + public static boolean saveMessageWithOptionalAttachments(UUID messageId, UUID senderId, UUID receiverId, + String receiverType, String content, String messageType, + List attachments) { + Connection conn = null; + try { + conn = ConnectionDb.connect(); + conn.setAutoCommit(false); + + boolean isText = "TEXT".equalsIgnoreCase(messageType); + if (isText) { + if (attachments != null && !attachments.isEmpty()) + throw new IllegalArgumentException("TEXT must not have attachments"); + if (content == null || content.isBlank()) + throw new IllegalArgumentException("TEXT must have non-empty content"); + } else { + if (attachments == null || attachments.isEmpty()) + throw new IllegalArgumentException("Non-TEXT must have at least one attachment"); + } + + insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType); + if (!isText) insertAttachmentsTx(conn, messageId, attachments); + + conn.commit(); + return true; + } catch (Exception e) { + if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} + e.printStackTrace(); + return false; + } finally { + if (conn != null) { + try { conn.setAutoCommit(true); } catch (SQLException ignored) {} + try { conn.close(); } catch (SQLException ignored) {} + } + } + } + + public static void markGloballyDeleted(UUID chatId) { String sql = "UPDATE messages SET is_deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'"; try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { @@ -388,20 +469,27 @@ public class MessageDatabase { 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 = ? + LEFT JOIN deleted_messages d ON m.message_id = d.message_id AND d.user_id = ? WHERE r.user_id IS NULL 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 = ? - )) + (m.receiver_type = 'private' AND EXISTS ( + SELECT 1 + FROM private_chat pc + WHERE pc.chat_id = m.receiver_id + AND (pc.user1_id = ? OR pc.user2_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 """; @@ -409,34 +497,34 @@ public class MessageDatabase { try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { - 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 + int i = 1; + stmt.setObject(i++, userId); // 1) receipts + stmt.setObject(i++, userId); // 2) deleted + stmt.setObject(i++, userId); // 3) private: pc.user1_id + stmt.setObject(i++, userId); // 4) private: pc.user2_id + stmt.setObject(i++, userId); // 5) group: gm.user_id + stmt.setObject(i++, userId); // 6) channel: cs.user_id - ResultSet rs = stmt.executeQuery(); - while (rs.next()) { - 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"), - 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 - ); - - messages.add(message); + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + messages.add(new Message( + UUID.fromString(rs.getString("message_id")), + rs.getObject("sender_id") != null ? UUID.fromString(rs.getString("sender_id")) : null, + rs.getString("receiver_type"), + UUID.fromString(rs.getString("receiver_id")), + rs.getString("content"), + rs.getString("message_type"), + rs.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(); } @@ -448,30 +536,34 @@ public class MessageDatabase { public static List getAttachments(UUID messageId) { List attachments = new ArrayList<>(); - String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?"; - + String sql = "SELECT file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url " + + "FROM message_attachments WHERE message_id = ? ORDER BY uploaded_at"; try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setObject(1, messageId); ResultSet rs = stmt.executeQuery(); - while (rs.next()) { attachments.add(new FileAttachment( rs.getString("file_url"), - rs.getString("file_type") + rs.getString("file_type"), + rs.getString("file_name"), + (Long) rs.getObject("file_size"), + rs.getString("mime_type"), + (Integer) rs.getObject("width"), + (Integer) rs.getObject("height"), + (Integer) rs.getObject("duration_seconds"), + rs.getString("thumbnail_url") )); } - } catch (SQLException e) { e.printStackTrace(); } - return attachments; } + public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) { String sql = """ SELECT MAX(send_at) FROM messages diff --git a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java index 430e457..8063ad4 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java +++ b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java @@ -1,14 +1,44 @@ package org.to.telegramfinalproject.Models; +import java.util.Objects; + public class FileAttachment { - private String fileUrl; - private String fileType; + 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; + + public FileAttachment(String fileUrl, + String fileType, + String fileName, + Long fileSize, + String mimeType, + Integer width, + Integer height, + Integer durationSeconds, + String thumbnailUrl) { + this.fileUrl = fileUrl; + this.fileType = fileType; + this.fileName = fileName; + this.fileSize = fileSize; + this.mimeType = mimeType; + this.width = width; + this.height = height; + this.durationSeconds = durationSeconds; + this.thumbnailUrl = thumbnailUrl; + } public FileAttachment(String fileUrl, String fileType) { this.fileUrl = fileUrl; this.fileType = fileType; } + // Getters public String getFileUrl() { return fileUrl; } @@ -16,4 +46,32 @@ public class FileAttachment { 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; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java b/src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java new file mode 100644 index 0000000..8a99837 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Server/UploadHttp.java @@ -0,0 +1,244 @@ +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 java.awt.image.BufferedImage; +import java.nio.file.*; + + + + +import java.nio.file.*; +import java.time.LocalDate; + + +// برای ویدیو (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 com.mpatric.mp3agic.Mp3File; + +public class UploadHttp { + + public static void start(int httpPort, String baseDir) throws IOException { + port(httpPort); + + Path basePath = Paths.get(baseDir).toAbsolutePath().normalize(); + Files.createDirectories(basePath); + staticFiles.externalLocation(basePath.toString()); + + post("/upload", (req, res) -> { + res.type("application/json"); + try { + long MAX_FILE = 25L * 1024 * 1024; // 25MB + req.attribute("org.eclipse.jetty.multipartConfig", + new MultipartConfigElement("/tmp", MAX_FILE, MAX_FILE, 0)); + + Part filePart = req.raw().getPart("file"); + if (filePart == null || filePart.getSize() == 0) { + res.status(400); + return jsonError("empty file"); + } + if (filePart.getSize() > MAX_FILE) { + res.status(413); + return jsonError("file too large"); + } + + String mime = filePart.getContentType(); + if (mime == null) { + res.status(415); + return jsonError("unknown mime"); + } + + String original = filePart.getSubmittedFileName(); + String ext = guessExt(original, mime); + String day = LocalDate.now().toString(); + String typeDir = subdirFor(mime); // images/videos/audios/files + String subdir = typeDir + "/" + day; + String name = java.util.UUID.randomUUID() + ext; + + Path dir = basePath.resolve(subdir).normalize(); + Files.createDirectories(dir); + Path target = dir.resolve(name).normalize(); + + try (InputStream in = filePart.getInputStream()) { + Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); + } finally { + filePart.delete(); + } + + String fileUrl = "/" + subdir.replace('\\', '/') + "/" + name; + String fileType = mapToFileType(mime); + + // متادیتا + 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) {} + } + } + + res.status(200); + return new JSONObject() + .put("file_url", fileUrl) + .put("file_type", fileType) + .put("file_name", original == null ? "" : safeName(original)) + .put("file_size", Files.size(target)) + .put("mime_type", mime) + .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) + .toString(); + + } catch (Exception e) { + e.printStackTrace(); // لوکال + res.status(500); + return jsonError("internal error"); + } + }); + + init(); + awaitInitialization(); + System.out.println("Upload HTTP server on http://localhost:" + httpPort + " baseDir=" + basePath); + } + + // ---------- Helpers ---------- + + private static String jsonError(String msg) { + return new JSONObject().put("error", msg).toString(); + } + + 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"; + } + + private static String mapToFileType(String mime) { + String m = mime.toLowerCase(); + if (m.startsWith("image/")) { + if (m.contains("gif")) return "GIF"; + return "IMAGE"; + } + if (m.startsWith("video/")) return "VIDEO"; + if (m.startsWith("audio/")) return "AUDIO"; + return "FILE"; + } + + 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; + } + 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 ("application/pdf".equalsIgnoreCase(mime)) return ".pdf"; + return ""; + } + + private static String safeName(String name) { + // پاک‌سازی خیلی ساده برای خروجی + return name.replace("\"", "").replace("\n", "").replace("\r", ""); + } + + private static int[] imageSize(Path file) { + try { + BufferedImage bi = ImageIO.read(file.toFile()); + if (bi != null) return new int[]{bi.getWidth(), bi.getHeight()}; + } catch (Exception ignore) {} + 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) { + 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) {} + + 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) {} + 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; + } + } +} From b3941108b5303229f9f7abf4f7166270d6a3a31a Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Mon, 11 Aug 2025 15:59:54 +0330 Subject: [PATCH 02/16] 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; - } - } } From ce4434650d2cf7ba56a37737632bdd8108bd72bb Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Tue, 12 Aug 2025 14:09:57 +0330 Subject: [PATCH 03/16] 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) { // From 82ade35450c41dca4d2708fdfdee70bd176cf503 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Tue, 12 Aug 2025 16:28:59 +0330 Subject: [PATCH 04/16] Clean code --- .../Client/SidebarHandler.java | 281 ------------------ 1 file changed, 281 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java b/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java index 5085e14..2960707 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/SidebarHandler.java @@ -255,234 +255,7 @@ public class SidebarHandler { actionHandler.showContactList(); } - public void getSavedMessagesData(String userId) { - try { - // Step 1: Create the request - JSONObject request = new JSONObject(); - request.put("action", "get_saved_messages"); - request.put("user_id", userId); - // Step 2: Send request and wait for response - JSONObject response = ActionHandler.sendWithResponse(request); - - // Step 3: Check the response - if (!response.optString("status", "fail").equals("success")) { - System.out.println("Failed to open Saved Messages chat: " + response.optString("message", "Unknown error")); - return; - } - - // Step 4: Extract "data" object - JSONObject data = response.getJSONObject("data"); - UUID chatId = UUID.fromString(data.getString("chat_id")); - JSONArray messagesArray = data.getJSONArray("messages"); - - // Step 5: Parse messages - List messages = new ArrayList<>(); - if (!messagesArray.isEmpty()) { - for (int i = 0; i < messagesArray.length(); i++) { - JSONObject msgJson = messagesArray.getJSONObject(i); - - // Safely extract optional UUIDs - UUID replyToId = null; - String replyToIdStr = msgJson.optString("reply_to_id", null); - if (replyToIdStr != null && !replyToIdStr.equals("null")) { - replyToId = UUID.fromString(replyToIdStr); - } - - UUID originalMessageId = null; - String originalMessageIdStr = msgJson.optString("original_message_id", null); - if (originalMessageIdStr != null && !originalMessageIdStr.equals("null")) { - originalMessageId = UUID.fromString(originalMessageIdStr); - } - - UUID forwardedBy = null; - String forwardedByStr = msgJson.optString("forwarded_by", null); - if (forwardedByStr != null && !forwardedByStr.equals("null")) { - forwardedBy = UUID.fromString(forwardedByStr); - } - - UUID forwardedFrom = null; - String forwardedFromStr = msgJson.optString("forwarded_from", null); - if (forwardedFromStr != null && !forwardedFromStr.equals("null")) { - forwardedFrom = UUID.fromString(forwardedFromStr); - } - - Message msg = new Message( - UUID.fromString(msgJson.getString("message_id")), - UUID.fromString(msgJson.getString("sender_id")), - msgJson.getString("receiver_type"), - UUID.fromString(msgJson.getString("receiver_id")), - msgJson.getString("content"), - msgJson.getString("message_type"), - LocalDateTime.parse(msgJson.getString("send_at").replace(" ", "T")), - msgJson.getString("status"), - replyToId, - msgJson.getBoolean("is_edited"), - originalMessageId, - forwardedBy, - forwardedFrom, - msgJson.getBoolean("is_deleted_globally"), - LocalDateTime.parse(msgJson.getString("edited_at").replace(" ", "T")) - ); - - messages.add(msg); - } - } - - // Step 6: Add to active chats if not already present - boolean alreadyExists = Session.activeChats.stream() - .anyMatch(entry -> entry.getId().equals(chatId)); - if (!alreadyExists) { - ChatEntry savedEntry = new ChatEntry( - chatId, - "Saved-Messages", - "Saved Messages", - "📌", // or use a URL string if you have an icon for saved messages - "private", - messages.isEmpty() ? null : messages.get(messages.size() - 1).getSend_at() - ); - savedEntry.setSavedMessages(true); - Session.activeChats.add(savedEntry); - } - - // Step 7: Show chat - showSavedMessages(chatId, messages); - - } catch (Exception e) { - System.out.println("An error occurred while retrieving Saved Messages."); - e.printStackTrace(); - } - } - - private void showSavedMessages(UUID chatId, List messages) { -// Scanner scanner = new Scanner(System.in); -// -// System.out.println("==== Saved Messages ===="); -// if (messages == null || messages.isEmpty()) { -// System.out.println("No messages yet."); -// } else { -// for (Message msg : messages) { -// System.out.println("[" + msg.getSend_at() + "] " + msg.getContent()); -// } -// } -// -// System.out.println("\n(Type your message below, or type 0 to exit)"); -// -// while (true) { -// System.out.print("You: "); -// String content = scanner.nextLine().trim(); -// if (content.equals("0")) { -// System.out.println("Exiting Saved Messages."); -// break; -// } -// -// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE / AUDIO): "); -// String messageType = scanner.nextLine().trim().toUpperCase(); -// Set allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE", "AUDIO"); -// while (!allowedTypes.contains(messageType)) { -// System.out.print("Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE / AUDIO): "); -// messageType = scanner.nextLine().trim().toUpperCase(); -// } -// -// // Attachments (اختیاری) -// 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().trim(); -// -// if (fileUrl.isEmpty()) { -// System.out.println("URL can not be empty. Try again."); -// continue; -// } -// if (fileUrl.contains(" ")) { -// System.out.println("URL cannot contain spaces. Try again."); -// continue; -// } -// if (!fileUrl.matches("^(http|https)://.*$")) { -// System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link."); -// continue; -// } -// -// System.out.print("File Type (IMAGE / VIDEO / FILE / AUDIO): "); -// String fileType = scanner.nextLine().trim().toUpperCase(); -// Set allowedFileTypes = Set.of("IMAGE", "VIDEO", "FILE", "AUDIO"); -// while (!allowedFileTypes.contains(fileType)) { -// System.out.print("Invalid type. Try again (IMAGE / VIDEO / FILE / AUDIO): "); -// fileType = scanner.nextLine().trim().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; -// } -// } -// -// // درخواست مطابق هندلر send_message -// JSONObject request = new JSONObject(); -// request.put("action", "send_message"); -// request.put("receiver_type", "private"); -// request.put("receiver_id", chatId.toString()); // chat_id -// request.put("content", content); -// request.put("message_type", messageType); -// if (attachmentsArray.length() > 0) { -// request.put("attachments", attachmentsArray); -// } -// -// JSONObject response = ActionHandler.sendWithResponse(request); -// if (!"success".equalsIgnoreCase(response.optString("status"))) { -// System.out.println("Failed to send message: " + response.optString("message", "Unknown error")); -// } else { -// System.out.println("Message sent."); -// -// Message justSent = new Message( -// UUID.fromString(response.getJSONObject("data").getString("message_id")), -// /* senderId */ userUUID, -// /* receiverId */ chatId, -// /* type */ "private", -// /* content */ content, -// /* msgType */ messageType, -// /* send_at */ java.time.LocalDateTime.now() -// ); -// messages.add(justSent); -// System.out.println("[" + justSent.getSend_at() + "] " + justSent.getContent()); -// } -// } - } - - -// public void openSavedMessages() { -// JSONObject req = new JSONObject().put("action", "get_or_create_saved_messages"); -// JSONObject res = sendWithResponse(req); -// if (res == null || !"success".equals(res.optString("status"))) { -// System.out.println("❌ Could not open Saved Messages: " + res.optString("message","")); -// return; -// } -// -// ActionHandler.requestChatList(); -// String chatId = res.getJSONObject("data").getString("chat_id"); -// -// JSONObject mreq = new JSONObject() -// .put("action", "get_messages") -// .put("receiver_type", "private") -// .put("receiver_id", chatId) -// .put("offset", 0) -// .put("limit", 50); -// -// JSONObject mres = sendWithResponse(mreq); -// JSONArray msgs = (mres != null && mres.has("data")) -// ? mres.getJSONObject("data").optJSONArray("messages") -// : new JSONArray(); -// -// List messages = parseMessages(msgs); // تبدیل JSON → Message -// showSavedMessages(UUID.fromString(chatId), messages); -// } -// @@ -774,7 +547,6 @@ public class SidebarHandler { } private String padBoxLine(String text, int width) { - // عرض: width، دو طرف │ │ final int inner = width - 2; if (text.length() > inner) { text = text.substring(0, inner - 1) + "…"; @@ -804,58 +576,5 @@ public class SidebarHandler { return approved ; } - private List parseMessages(JSONArray msgs) { - List list = new ArrayList<>(); - if (msgs == null) return list; - for (int i = 0; i < msgs.length(); i++) { - try { - JSONObject obj = msgs.getJSONObject(i); - - UUID messageId = UUID.fromString(obj.getString("message_id")); - UUID senderId = UUID.fromString(obj.getString("sender_id")); - String receiverType= obj.getString("receiver_type"); // "private" | "group" | "channel" - UUID receiverId = UUID.fromString(obj.getString("receiver_id")); // برای private = chat_id - String content = obj.optString("content", ""); - String messageType = obj.optString("message_type", "TEXT"); - LocalDateTime sent = LocalDateTime.parse(obj.getString("send_at")); - - Message m = new Message( - messageId, senderId, receiverId, receiverType, content, messageType, sent - ); - - if (obj.has("status") && !obj.isNull("status")) { - try { m.setStatus(obj.getString("status")); } catch (Exception ignore) {} - } - if (obj.has("reply_to_id") && !obj.isNull("reply_to_id")) { - try { m.setReply_to_id(UUID.fromString(obj.getString("reply_to_id"))); } catch (Exception ignore) {} - } - if (obj.has("forwarded_by") && !obj.isNull("forwarded_by")) { - try { m.setForwarded_by(UUID.fromString(obj.getString("forwarded_by"))); } catch (Exception ignore) {} - } - if (obj.has("forwarded_from") && !obj.isNull("forwarded_from")) { - try { m.setForwarded_from(UUID.fromString(obj.getString("forwarded_from"))); } catch (Exception ignore) {} - } - -// // ضمیمه‌ها (اگر در مدل Message متد addAttachment داری) -// if (obj.has("attachments") && !obj.isNull("attachments")) { -// try { -// JSONArray atts = obj.getJSONArray("attachments"); -// for (int j = 0; j < atts.length(); j++) { -// JSONObject a = atts.getJSONObject(j); -// String fileUrl = a.getString("file_url"); -// String fileType = a.getString("file_type"); -// FileAttachment fa = new FileAttachment(fileUrl, fileType); -// try { m.addAttachment(fa); } catch (Exception ignore) {} -// } -// } catch (Exception ignore) {} -// } - - list.add(m); - } catch (Exception perItem) { - perItem.printStackTrace(); - } - } - return list; - } } \ No newline at end of file From f49dc171f5806e8e8540e67e5eaecdc1d308b2e5 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 13 Aug 2025 11:04:54 +0330 Subject: [PATCH 05/16] send file --- .../Client/ActionHandler.java | 135 +++++++++++++++++- .../Client/TelegramClient.java | 10 +- 2 files changed, 135 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index a78e3f5..5b287ad 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -9,9 +9,7 @@ import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; import org.to.telegramfinalproject.Models.SearchResultModel; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.PrintWriter; +import java.io.*; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ArrayBlockingQueue; @@ -26,6 +24,8 @@ public class ActionHandler { private final Scanner scanner; public static volatile boolean forceExitChat = false; public static ActionHandler instance; + private final DataOutputStream outBin; + @@ -34,12 +34,19 @@ public class ActionHandler { IncomingMessageListener listener = new IncomingMessageListener(this.in); listener.handleRealTimeEvent (json); } - public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) { +// public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) { +// this.out = out; +// this.in = in; +// this.scanner = scanner; +// ActionHandler.instance = this; +// +// } + public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) { this.out = out; this.in = in; + this.outBin = outBin; this.scanner = scanner; ActionHandler.instance = this; - } public void loginHandler() { @@ -3977,6 +3984,124 @@ public class ActionHandler { } + + public void sendMessageInteractive(UUID receiverId, String receiverType) { + Scanner sc = new Scanner(System.in); + + System.out.print("Type (TEXT / IMAGE / AUDIO): "); + String type = sc.nextLine().trim().toUpperCase(); + while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) { + System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): "); + type = sc.nextLine().trim().toUpperCase(); + } + + System.out.print("Text (optional for media; empty = no caption): "); + String text = sc.nextLine(); + + if ("TEXT".equals(type)) { + sendTextMessage(receiverId, receiverType, text); + } else { + System.out.print("File path: "); + String path = sc.nextLine().trim(); + File f = new File(path); + if (!f.isFile()) { + System.out.println("❌ File not found"); + return; + } + try { + sendMediaMessage(receiverId, receiverType, type, f, text); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("❌ Media send failed: " + e.getMessage()); + } + } + } + + private void sendTextMessage(UUID receiverId, String receiverType, String content) { + org.json.JSONObject msg = new org.json.JSONObject() + .put("action", "send_message") + .put("receiver_type", receiverType) + .put("receiver_id", receiverId.toString()) + .put("message_type", "TEXT") + .put("content", content == null ? "" : content); + + sendWithResponse(msg); + try { + String line = in.readLine(); // Ack + if (line == null) { System.out.println("❌ No response"); return; } + org.json.JSONObject resp = new org.json.JSONObject(line); + if ("success".equalsIgnoreCase(resp.optString("status"))) { + System.out.println("✅ Sent. id=" + resp.optJSONObject("data").optString("message_id","")); + } else { + System.out.println("❌ Failed: " + resp.optString("message")); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void sendMediaMessage(UUID receiverId, String receiverType, String type /*IMAGE/AUDIO*/, File file, String caption) { + try { + String mime = java.nio.file.Files.probeContentType(file.toPath()); + if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; + + UUID messageId = UUID.randomUUID(); + + JSONObject header = new JSONObject() + .put("message_id", messageId.toString()) + .put("sender_id", TelegramClient.loggedInUserId.toString()) + .put("receiver_type", receiverType) // private|group|channel + .put("receiver_id", receiverId.toString()) + .put("message_type", type.toUpperCase()) // IMAGE | AUDIO + .put("file_name", file.getName()) + .put("mime_type", mime) + .put("text", caption == null ? "" : caption); + + byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + long contentLen = file.length(); + + // قبل از ارسال، صف برای ACK ثبت کن + BlockingQueue q = new LinkedBlockingQueue<>(1); + TelegramClient.pendingResponses.put(messageId.toString(), q); + + // 1) خط سوئیچ به MEDIA + out.print("MEDIA\n"); + out.flush(); + + // 2) فریم باینری: magic + headerLen + header + contentLen + content + outBin.writeInt(0x4D444D31); // "MDM1" + outBin.writeInt(headerBytes.length); // headerLen + outBin.write(headerBytes); // header + outBin.writeLong(contentLen); // contentLen + + try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { + byte[] buf = new byte[8192]; + int n; + while ((n = fis.read(buf)) != -1) { + outBin.write(buf, 0, n); + } + } + outBin.flush(); + + // 3) منتظر ACK از Listener (با message_id) + JSONObject ack = q.take(); // بلاک تا بیاد + TelegramClient.pendingResponses.remove(messageId.toString()); + + if ("success".equalsIgnoreCase(ack.optString("status"))) { + System.out.println("✅ Media sent. id=" + ack.optString("message_id") + + " url=" + ack.optString("file_url")); + } else { + System.out.println("❌ Media failed: " + ack.optString("message")); + } + } catch (Exception e) { + e.printStackTrace(); + System.out.println("❌ sendMediaMessage error: " + e.getMessage()); + } + } + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 8db405e..74b1b59 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -2,10 +2,7 @@ package org.to.telegramfinalproject.Client; import org.json.JSONObject; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; +import java.io.*; import java.net.Socket; import java.util.Map; import java.util.Scanner; @@ -37,14 +34,17 @@ public class TelegramClient { public static TelegramClient getInstance() { return instance; } + private DataOutputStream outBin; public void start() { try { socket = new Socket(SERVER_HOST, SERVER_PORT); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); + outBin = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); // برای MEDIA + System.out.println("✅ Connected to Telegram Server"); - handler = new ActionHandler(out, in, scanner); + handler = new ActionHandler(out, in, outBin, scanner); Thread listenerThread = new Thread(new IncomingMessageListener(in)); listenerThread.setDaemon(true); From eb633b5e1391c53326a062837029b8ff1c947779 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 13 Aug 2025 17:43:43 +0330 Subject: [PATCH 06/16] Work on sending files --- .../Client/ActionHandler.java | 134 +++++++++++------- .../Client/IncomingMessageListener.java | 11 ++ .../Client/MediaSender.java | 2 +- .../Client/TelegramClient.java | 2 +- .../Server/ClientHandler.java | 38 +++-- .../Server/MainServer.java | 2 +- 6 files changed, 123 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 5b287ad..6a8ff7e 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -2,20 +2,15 @@ package org.to.telegramfinalproject.Client; import org.json.JSONArray; import org.json.JSONObject; -import org.to.telegramfinalproject.Database.PrivateChatDatabase; -import org.to.telegramfinalproject.Database.ContactDatabase; import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; -import org.to.telegramfinalproject.Models.SearchResultModel; import java.io.*; import java.time.LocalDateTime; import java.util.*; -import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; public class ActionHandler { @@ -1434,7 +1429,7 @@ public class ActionHandler { String input = scanner.nextLine(); switch (input) { - case "1" -> sendMessage(chat.getId(), "private"); + case "1" -> sendMessageInteractive(chat.getId(), "private"); case "2" -> toggleBlock(chat.getOtherUserId()); case "3" -> { deleteChat(chat.getId(), false); @@ -1551,7 +1546,7 @@ public class ActionHandler { String input = scanner.nextLine(); switch (input) { - case "1" -> sendMessage(chat.getId(), "group"); + case "1" -> sendMessageInteractive(chat.getId(), "group"); case "2" -> viewGroupMembers(chat.getId()); case "3" -> { if (isOwner || (isAdmin && perms.optBoolean("can_add_members", false))) @@ -1678,7 +1673,7 @@ public class ActionHandler { switch (input) { case "1" -> { if (isOwner || (isAdmin && perms.optBoolean("can_post", false))) { - sendMessage(chat.getId(), "channel"); + sendMessageInteractive(chat.getId(), "channel"); } else { System.out.println("❌ You don't have permission to post."); } @@ -4018,42 +4013,49 @@ public class ActionHandler { } private void sendTextMessage(UUID receiverId, String receiverType, String content) { - org.json.JSONObject msg = new org.json.JSONObject() + JSONObject req = new JSONObject() .put("action", "send_message") .put("receiver_type", receiverType) .put("receiver_id", receiverId.toString()) .put("message_type", "TEXT") .put("content", content == null ? "" : content); - sendWithResponse(msg); - try { - String line = in.readLine(); // Ack - if (line == null) { System.out.println("❌ No response"); return; } - org.json.JSONObject resp = new org.json.JSONObject(line); - if ("success".equalsIgnoreCase(resp.optString("status"))) { - System.out.println("✅ Sent. id=" + resp.optJSONObject("data").optString("message_id","")); - } else { - System.out.println("❌ Failed: " + resp.optString("message")); - } - } catch (Exception e) { - e.printStackTrace(); + JSONObject resp = sendWithResponse(req); + if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) { + System.out.println("✅ Sent. id=" + resp.optJSONObject("data").optString("message_id","")); + } else { + System.out.println("❌ Failed: " + (resp != null ? resp.optString("message") : "no response")); } } - public void sendMediaMessage(UUID receiverId, String receiverType, String type /*IMAGE/AUDIO*/, File file, String caption) { - try { - String mime = java.nio.file.Files.probeContentType(file.toPath()); - if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; + public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) { + // اعتبارسنجی ورودی فایل (اگر ورودی از یوزر میاد، قبل از ساخت File کوتیشن‌ها رو حذف کن) + if (file == null) { + System.out.println("❌ File is null"); + return; + } + if (!file.exists()) { + System.out.println("❌ File not found: " + file.getAbsolutePath()); + return; + } + if (file.isDirectory()) { + System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath()); + return; + } - UUID messageId = UUID.randomUUID(); + final UUID messageId = UUID.randomUUID(); + + try { + String mime = detectMime(file, type.toUpperCase()); + if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; JSONObject header = new JSONObject() .put("message_id", messageId.toString()) - .put("sender_id", TelegramClient.loggedInUserId.toString()) - .put("receiver_type", receiverType) // private|group|channel + .put("sender_id", TelegramClient.loggedInUserId.toString()) + .put("receiver_type", receiverType) // private|group|channel .put("receiver_id", receiverId.toString()) - .put("message_type", type.toUpperCase()) // IMAGE | AUDIO + .put("message_type", type.toUpperCase()) // IMAGE | AUDIO .put("file_name", file.getName()) .put("mime_type", mime) .put("text", caption == null ? "" : caption); @@ -4061,39 +4063,50 @@ public class ActionHandler { byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); long contentLen = file.length(); - // قبل از ارسال، صف برای ACK ثبت کن + // صف ACK BlockingQueue q = new LinkedBlockingQueue<>(1); TelegramClient.pendingResponses.put(messageId.toString(), q); - // 1) خط سوئیچ به MEDIA - out.print("MEDIA\n"); - out.flush(); + try { + // ⛔ مهم: فقط از outBin استفاده کن تا بافرها قاطی نشن + // 1) خط سوئیچ به MEDIA + outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); + outBin.flush(); - // 2) فریم باینری: magic + headerLen + header + contentLen + content - outBin.writeInt(0x4D444D31); // "MDM1" - outBin.writeInt(headerBytes.length); // headerLen - outBin.write(headerBytes); // header - outBin.writeLong(contentLen); // contentLen + // 2) فریم باینری: magic + headerLen + header + contentLen + content + outBin.writeInt(0x4D444D31); // "MDM1" + outBin.writeInt(headerBytes.length); // headerLen (int) + outBin.write(headerBytes); // header + outBin.writeLong(contentLen); // contentLen (long) ← مطمئن شو سرور هم Long می‌خونه - try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { - byte[] buf = new byte[8192]; - int n; - while ((n = fis.read(buf)) != -1) { - outBin.write(buf, 0, n); + try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { + byte[] buf = new byte[8192]; + int n; + while ((n = fis.read(buf)) != -1) { + outBin.write(buf, 0, n); + } } - } - outBin.flush(); + outBin.flush(); - // 3) منتظر ACK از Listener (با message_id) - JSONObject ack = q.take(); // بلاک تا بیاد - TelegramClient.pendingResponses.remove(messageId.toString()); + // 3) منتظر ACK با تایم‌اوت (فقط یکی!) + JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); + if (ack == null) { + System.out.println("❌ Media ACK timeout for " + messageId); + return; + } - if ("success".equalsIgnoreCase(ack.optString("status"))) { - System.out.println("✅ Media sent. id=" + ack.optString("message_id") + - " url=" + ack.optString("file_url")); - } else { - System.out.println("❌ Media failed: " + ack.optString("message")); + String status = ack.optString("status", "error"); + if ("success".equalsIgnoreCase(status)) { + System.out.println("✅ Media sent. id=" + ack.optString("message_id") + + " url=" + ack.optString("file_url")); + } else { + System.out.println("❌ Media failed: " + ack.optString("message")); + } + + } finally { + TelegramClient.pendingResponses.remove(messageId.toString()); } + } catch (Exception e) { e.printStackTrace(); System.out.println("❌ sendMediaMessage error: " + e.getMessage()); @@ -4101,6 +4114,21 @@ public class ActionHandler { } + private static String detectMime(File f, String typeUpper /* IMAGE or AUDIO */) { + try { + String m = java.nio.file.Files.probeContentType(f.toPath()); + if (m != null) return m; + } catch (Exception ignored) {} + String name = f.getName().toLowerCase(); + if (name.endsWith(".png")) return "image/png"; + if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg"; + if (name.endsWith(".gif")) return "image/gif"; + if (name.endsWith(".mp3")) return "audio/mpeg"; + if (name.endsWith(".wav")) return "audio/wav"; + if (name.endsWith(".ogg")) return "audio/ogg"; + return typeUpper.equals("IMAGE") ? "image/*" : "audio/*"; + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index e6882a2..2ddbb4a 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -28,6 +28,17 @@ public class IncomingMessageListener implements Runnable { JSONObject response = new JSONObject(line); System.out.println("📥 Received raw line: " + line); + + //for media + String mid = response.optString("message_id", ""); + if (!mid.isEmpty()) { + BlockingQueue q = TelegramClient.pendingResponses.get(mid); + if (q != null) { + q.put(response); + continue; + } + } + //if it has reqID answer if (response.has("request_id")) { String requestId = response.getString("request_id"); diff --git a/src/main/java/org/to/telegramfinalproject/Client/MediaSender.java b/src/main/java/org/to/telegramfinalproject/Client/MediaSender.java index 3eec675..112fd0d 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/MediaSender.java +++ b/src/main/java/org/to/telegramfinalproject/Client/MediaSender.java @@ -10,7 +10,7 @@ import java.awt.image.BufferedImage; -public class MediaSender { +class MediaSender { public static void sendImageOrAudio(Socket socket, UUID senderId, diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 74b1b59..464b36e 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -13,7 +13,7 @@ import java.util.concurrent.LinkedBlockingQueue; public class TelegramClient { private static final String SERVER_HOST = "localhost"; - private static final int SERVER_PORT = 8000; + private static final int SERVER_PORT = 8080; private static Socket socket; private BufferedReader in; private PrintWriter out; diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index bdad647..3ef4a43 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -32,7 +32,7 @@ public class ClientHandler implements Runnable { // 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 و هدرهای باینری + DataInputStream dis = new DataInputStream(bis); //for binary headers PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8), true); @@ -2871,8 +2871,7 @@ public class ClientHandler implements Runnable { String receiverType = json.getString("receiver_type"); UUID receiverId = UUID.fromString(json.getString("receiver_id")); - // private validations... - // ... + String content = json.optString("content", ""); String messageType = json.optString("message_type", "TEXT"); @@ -2935,7 +2934,6 @@ public class ClientHandler implements Runnable { // 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(); @@ -2969,21 +2967,41 @@ public class ClientHandler implements Runnable { if (sender != null) data.put("sender_name", sender.getProfile_name()); payload.put("data", data); - List receivers = getReceiversForChat(receiverId, receiverType); - receivers.remove(senderId); - RealTimeEventDispatcher.broadcastToUsers(receivers, payload); +// List receivers = getReceiversForChat(receiverId, receiverType); +// receivers.remove(senderId); +// RealTimeEventDispatcher.broadcastToUsers(receivers, payload); +// +// +// +// // 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() +// .put("action", "chat_updated") +// .put("data", chatUpdate); +// +// for (UUID r : receivers) RealTimeEventDispatcher.sendToUser(r, chatPayload); - // chat_updated + + List allMembers = getReceiversForChat(receiverId, receiverType); // شامل sender + // به همه 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() .put("action", "chat_updated") .put("data", chatUpdate); + for (UUID u : allMembers) RealTimeEventDispatcher.sendToUser(u, chatPayload); + + // حالا new_message را فقط به غیر از sender + List others = new ArrayList<>(allMembers); + others.remove(senderId); + RealTimeEventDispatcher.broadcastToUsers(others, payload); - for (UUID r : receivers) RealTimeEventDispatcher.sendToUser(r, chatPayload); JSONObject respData = new JSONObject().put("message_id", messageId.toString()); return new ResponseModel("success", "Message sent successfully.", respData); diff --git a/src/main/java/org/to/telegramfinalproject/Server/MainServer.java b/src/main/java/org/to/telegramfinalproject/Server/MainServer.java index abf1de0..a7882a3 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/MainServer.java +++ b/src/main/java/org/to/telegramfinalproject/Server/MainServer.java @@ -7,7 +7,7 @@ import java.net.ServerSocket; import java.net.Socket; public class MainServer { - private static final int PORT = 8000; + private static final int PORT = 8080; public static void main(String[] args) { From d4a6832ff8f2573fcdb4c74b49bf5b8a3c164f25 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 13 Aug 2025 20:18:14 +0330 Subject: [PATCH 07/16] Update init.sql --- src/main/resources/init.sql | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql index e5b9720..5325bc0 100644 --- a/src/main/resources/init.sql +++ b/src/main/resources/init.sql @@ -156,5 +156,14 @@ CREATE TABLE IF NOT EXISTS message_attachments ( ); +--Run this part in your pg +ALTER TABLE message_attachments + ADD COLUMN IF NOT EXISTS media_key UUID; +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +UPDATE message_attachments +SET media_key = gen_random_uuid() +WHERE media_key IS NULL; + From 5689cb4267db330766c7fc2c1939d0bc1407bcda Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 13 Aug 2025 21:19:50 +0330 Subject: [PATCH 08/16] Work on sending files --- .../Client/ActionHandler.java | 2 +- .../Database/MessageDatabase.java | 87 +++++-- .../Models/FileAttachment.java | 56 ++++- .../Server/ClientHandler.java | 228 ++++++++++++++---- 4 files changed, 295 insertions(+), 78 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 6a8ff7e..6f06582 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1313,7 +1313,7 @@ public class ActionHandler { JSONObject m = messages.getJSONObject(i); String senderId = m.getString("sender_id"); String senderName = m.optString("sender_name", "Other"); - String content = m.getString("content"); + String content = m.optString("content", ""); String time = m.getString("send_at"); String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 7c8a35b..90ff0fc 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -84,25 +84,54 @@ public class MessageDatabase { public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List attachments) throws SQLException { if (attachments == null || attachments.isEmpty()) return true; - String sql = """ - INSERT INTO message_attachments - (attachment_id, message_id, file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + + final String sql = """ + INSERT INTO message_attachments( + attachment_id, message_id, + file_url, file_type, file_name, file_size, mime_type, + width, height, duration_seconds, thumbnail_url, + media_key, storage_path + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) """; + try (PreparedStatement ps = conn.prepareStatement(sql)) { for (FileAttachment att : attachments) { - ps.setObject(1, UUID.randomUUID()); - ps.setObject(2, messageId); - ps.setString(3, att.getFileUrl()); - ps.setString(4, att.getFileType()); // IMAGE/VIDEO/AUDIO/FILE/GIF/STICKER - ps.setString(5, att.getFileName()); - if (att.getFileSize() != null) ps.setLong(6, att.getFileSize()); else ps.setNull(6, java.sql.Types.BIGINT); - ps.setString(7, att.getMimeType()); - if (att.getWidth() != null) ps.setInt(8, att.getWidth()); else ps.setNull(8, java.sql.Types.INTEGER); - if (att.getHeight() != null) ps.setInt(9, att.getHeight()); else ps.setNull(9, java.sql.Types.INTEGER); - if (att.getDurationSeconds() != null) ps.setInt(10, att.getDurationSeconds()); else ps.setNull(10, java.sql.Types.INTEGER); - ps.setString(11, att.getThumbnailUrl()); + if (att == null) throw new IllegalArgumentException("Attachment is null"); + UUID attachmentId = att.getAttachmentId() != null ? att.getAttachmentId() : UUID.randomUUID(); + UUID mediaKey = att.getMediaKey() != null ? att.getMediaKey() : attachmentId; // ساده‌ترین حالت + + String ft = att.getFileType(); + if (!"IMAGE".equalsIgnoreCase(ft) && !"AUDIO".equalsIgnoreCase(ft)) { + throw new IllegalArgumentException("file_type must be IMAGE or AUDIO"); + } + if (att.getStoragePath() == null || att.getStoragePath().isBlank()) { + throw new IllegalArgumentException("storage_path is required for socket downloads"); + } + + int i = 1; + ps.setObject(i++, attachmentId); + ps.setObject(i++, messageId); + //file url (display link) + if (att.getFileUrl() == null || att.getFileUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR); + else ps.setString(i++, att.getFileUrl()); + + ps.setString(i++, ft.toUpperCase()); + ps.setString(i++, att.getFileName()); + if (att.getFileSize() == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, att.getFileSize()); + if (att.getMimeType() == null) ps.setNull(i++, java.sql.Types.VARCHAR); else ps.setString(i++, att.getMimeType()); + if (att.getWidth() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getWidth()); + if (att.getHeight() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getHeight()); + if (att.getDurationSeconds() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getDurationSeconds()); + if (att.getThumbnailUrl() == null || att.getThumbnailUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR); + else ps.setString(i++, att.getThumbnailUrl()); + + ps.setObject(i++, mediaKey); + ps.setString(i++, att.getStoragePath()); + ps.addBatch(); + + att.setAttachmentId(attachmentId); + att.setMediaKey(mediaKey); } ps.executeBatch(); return true; @@ -110,15 +139,24 @@ public class MessageDatabase { } - public static boolean saveMessageWithOptionalAttachments(UUID messageId, UUID senderId, UUID receiverId, - String receiverType, String content, String messageType, - List attachments) { + + public static boolean saveMessageWithOptionalAttachments( + UUID messageId, UUID senderId, UUID receiverId, + String receiverType, String content, String messageType, + List attachments + ) { Connection conn = null; try { conn = ConnectionDb.connect(); conn.setAutoCommit(false); - boolean isText = "TEXT".equalsIgnoreCase(messageType); + boolean isText = "TEXT".equalsIgnoreCase(messageType); + boolean isImage = "IMAGE".equalsIgnoreCase(messageType); + boolean isAudio = "AUDIO".equalsIgnoreCase(messageType); + if (!isText && !isImage && !isAudio) { + throw new IllegalArgumentException("messageType must be TEXT, IMAGE, or AUDIO"); + } + if (isText) { if (attachments != null && !attachments.isEmpty()) throw new IllegalArgumentException("TEXT must not have attachments"); @@ -127,9 +165,18 @@ public class MessageDatabase { } else { if (attachments == null || attachments.isEmpty()) throw new IllegalArgumentException("Non-TEXT must have at least one attachment"); + + for (FileAttachment a : attachments) { + if (a == null) throw new IllegalArgumentException("Attachment is null"); + String ft = a.getFileType(); + if (isImage && !"IMAGE".equalsIgnoreCase(ft)) + throw new IllegalArgumentException("All attachments must be IMAGE for messageType=IMAGE"); + if (isAudio && !"AUDIO".equalsIgnoreCase(ft)) + throw new IllegalArgumentException("All attachments must be AUDIO for messageType=AUDIO"); + } } - insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType); + insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType.toUpperCase()); if (!isText) insertAttachmentsTx(conn, messageId, attachments); conn.commit(); diff --git a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java index 58f7602..f3f5d9c 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java +++ b/src/main/java/org/to/telegramfinalproject/Models/FileAttachment.java @@ -2,18 +2,22 @@ package org.to.telegramfinalproject.Models; import org.json.JSONObject; import java.util.Objects; +import java.util.UUID; public class FileAttachment { - 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; + private UUID attachmentId; // اختیاری؛ اگر null بود، تولید می‌کنیم + private UUID mediaKey; + private String fileUrl; + private String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER + private String fileName; + private Long fileSize; + private String mimeType; // e.g., image/png + private Integer width; + private Integer height; + private Integer durationSeconds; // for audio/video + private String thumbnailUrl; + private String storagePath; public FileAttachment(String fileUrl, String fileType, @@ -39,6 +43,10 @@ public class FileAttachment { this(fileUrl, fileType, null, null, null, null, null, null, null); } + public FileAttachment() { + } + + // ساخت از JSON /upload public static FileAttachment fromUploadJson(JSONObject j) { return new FileAttachment( @@ -54,7 +62,6 @@ public class FileAttachment { ); } - // خروجی JSON برای RT/کلاینت public JSONObject toJson() { JSONObject out = new JSONObject() .put("file_url", fileUrl) @@ -123,4 +130,33 @@ public class FileAttachment { ", thumbnailUrl='" + thumbnailUrl + '\'' + '}'; } + + public UUID getAttachmentId() {return attachmentId; + } + + public UUID getMediaKey() {return mediaKey; + } + + public String getStoragePath() {return storagePath; + } + + public void setAttachmentId(UUID attachmentId) {this.attachmentId = attachmentId; + } + + public void setMediaKey(UUID mediaKey) {this.mediaKey = mediaKey; + } + + + public void setFileUrl(String fileUrl) {this.fileUrl = fileUrl; + } + public void setFileType(String fileType){this.fileType = fileType;} + public void setFileName(String fileName){this.fileName = fileName;} + public void setFileSize(Long fileSize){this.fileSize = fileSize;} + public void setMimeType(String mimeType){this.mimeType = mimeType;} + public void setWidth(int width){this.width = width;} + public void setHeight(int height){this.height = height;} + public void setDurationSeconds(Integer durationSeconds){this.durationSeconds = durationSeconds;} + public void setThumbnailUrl(String thumbnailUrl){this.thumbnailUrl = thumbnailUrl;} + public void setStoragePath(String storagePath) {this.storagePath = storagePath; + } } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 3ef4a43..76136db 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -11,6 +11,9 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil; import java.io.*; import java.net.Socket; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.LocalDateTime; import java.util.*; @@ -2609,68 +2612,177 @@ public class ClientHandler implements Runnable { } +// 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 void handleMediaFrame(DataInputStream dis, PrintWriter out) { try { - // MAGIC = "MDM1" - final int MAGIC_EXPECTED = 0x4D444D31; + final int MAGIC_EXPECTED = 0x4D444D31; // "MDM1" int magic = dis.readInt(); if (magic != MAGIC_EXPECTED) { - out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); - out.flush(); - return; + 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; + 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; + 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; + 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 + 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").toUpperCase(); // IMAGE | AUDIO - if (!"IMAGE".equalsIgnoreCase(messageType) && !"AUDIO".equalsIgnoreCase(messageType)) { + if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType)) { skip(dis, contentLen); - out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); - out.flush(); - return; + 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", ""); + 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; + 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 kind = "IMAGE".equals(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); @@ -2679,7 +2791,7 @@ public class ClientHandler implements Runnable { 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; @@ -2694,30 +2806,52 @@ public class ClientHandler implements Runnable { } 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 - ); + String storagePath = target.toString(); // فقط سرور استفاده کنه + String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; // اختیاری/نمایشی (HTTP لازم نیست) + String mt = messageType; // "IMAGE" یا "AUDIO" + int safeWidth = ("IMAGE".equals(mt) && width != null) ? width : 0; + int safeHeight = ("IMAGE".equals(mt) && height != null) ? height : 0; + FileAttachment att = new FileAttachment(); + att.setFileUrl(fileUrl); // اختیاری + att.setFileType(messageType); // IMAGE/AUDIO + att.setFileName(fileName); + att.setFileSize(fileSize); + att.setMimeType(mimeType); + att.setWidth(safeWidth); + att.setHeight(safeHeight); + att.setDurationSeconds(0); + att.setThumbnailUrl(null); + att.setStoragePath(storagePath); // اجباری برای سوکت + // اجازه بده insertAttachmentsTx برایش mediaKey و attachmentId بسازد + java.util.List atts = java.util.List.of(att); boolean ok = MessageDatabase.saveMessageWithOptionalAttachments( - messageId, senderId, receiverId, rType, text, messageType.toUpperCase(), java.util.List.of(att) + messageId, senderId, receiverId, rType, text, messageType, atts ); + UUID mediaKey = null; + try (PreparedStatement q = ConnectionDb.connect().prepareStatement( + "SELECT media_key FROM message_attachments WHERE message_id = ? AND storage_path = ? LIMIT 1" + )) { + q.setObject(1, messageId); + q.setString(2, storagePath); + try (ResultSet rs = q.executeQuery()) { + if (rs.next()) mediaKey = (UUID) rs.getObject(1); + } + } catch (SQLException sqle) { + // در بدترین حالت بدون media_key ACK می‌دیم، ولی بهتره خطا رو لاگ کنیم + sqle.printStackTrace(); + } + JSONObject ack = new JSONObject() .put("status", ok ? "success" : "error") .put("message_id", messageId.toString()) - .put("file_url", fileUrl) + .put("media_key", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) // برای دانلود سوکتی + .put("file_name", fileName) .put("file_size", fileSize) - .put("mime_type", mimeType); + .put("mime_type", mimeType) + .put("display_path", fileUrl); // صرفاً نمایشی out.println(ack.toString()); out.flush(); @@ -2729,6 +2863,7 @@ public class ClientHandler implements Runnable { } } + private static void skip(DataInputStream dis, long n) throws IOException { if (n <= 0) return; byte[] buf = new byte[8192]; @@ -2997,7 +3132,6 @@ public class ClientHandler implements Runnable { .put("data", chatUpdate); for (UUID u : allMembers) RealTimeEventDispatcher.sendToUser(u, chatPayload); - // حالا new_message را فقط به غیر از sender List others = new ArrayList<>(allMembers); others.remove(senderId); RealTimeEventDispatcher.broadcastToUsers(others, payload); From 27fc134ffe68c5b7f88a63f21097ed7ab76a586c Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Thu, 14 Aug 2025 19:48:29 +0330 Subject: [PATCH 09/16] Work on download files(private chats and groups) --- .../Client/ActionHandler.java | 200 +++++++++++++++++- .../Client/DownloadIndexRegistry.java | 30 +++ .../Client/DownloadsIndex.java | 122 +++++++++++ .../Client/IncomingMessageListener.java | 181 ++++++++-------- .../telegramfinalproject/Client/Session.java | 5 +- .../Client/SocketMediaDownloader.java | 78 +++++++ .../Client/TelegramClient.java | 25 ++- .../Database/ChannelDatabase.java | 5 +- .../Database/MessageDatabase.java | 173 ++++++++++++++- .../Database/MessageReactionDatabase.java | 6 + .../Database/PrivateChatDatabase.java | 23 ++ .../telegramfinalproject/Models/MediaRow.java | 22 ++ .../Server/ClientHandler.java | 137 +++++++++++- 13 files changed, 888 insertions(+), 119 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java create mode 100644 src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java create mode 100644 src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java create mode 100644 src/main/java/org/to/telegramfinalproject/Models/MediaRow.java diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 6f06582..7d65dc0 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -7,6 +7,9 @@ import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.BlockingQueue; @@ -23,19 +26,11 @@ public class ActionHandler { - - private void handleRealTime(JSONObject json) throws IOException { IncomingMessageListener listener = new IncomingMessageListener(this.in); listener.handleRealTimeEvent (json); } -// public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) { -// this.out = out; -// this.in = in; -// this.scanner = scanner; -// ActionHandler.instance = this; -// -// } + public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) { this.out = out; this.in = in; @@ -477,6 +472,12 @@ public class ActionHandler { case "register": Session.currentUser = response.getJSONObject("data"); + //for downloaded medias + UUID accountId = UUID.fromString(Session.currentUser.getString("internal_uuid")); + Session.downloadsIndex = new DownloadsIndex(accountId); + + + JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); JSONArray Archived = Session.currentUser.getJSONArray("archived_chat_list"); JSONArray Active = Session.currentUser.getJSONArray("active_chat_list"); @@ -3648,6 +3649,18 @@ public class ActionHandler { replyLabel = "↪️ Reply to " + repliedSender + ": \"" + repliedContent + "\""; } + JSONArray atts = msg.optJSONArray("attachments"); + if (atts != null && atts.length() > 0) { + System.out.println(" 📎 " + atts.length() + " attachment(s)"); + for (int a = 0; a < atts.length(); a++) { + JSONObject att = atts.getJSONObject(a); + String fn = att.optString("file_name", "(unnamed)"); + long sz = att.optLong("file_size", 0); + System.out.printf(" - #%d %s (%s)\n", a + 1, fn, humanSize(sz)); + } + } + + JSONArray reactions = msg.optJSONArray("reactions"); if (reactions != null && !reactions.isEmpty()) { System.out.print(" 💬 Reactions: "); @@ -3697,6 +3710,10 @@ public class ActionHandler { boolean isSender = senderId.toString().equals(Session.currentUser.getString("internal_uuid")); boolean isChannel = chat.getType().equals("channel"); boolean isOwnerOrAdmin = chat.isOwner() || chat.isAdmin(); + //for media + JSONArray atts = selected.optJSONArray("attachments"); + boolean hasAttachments = (atts != null && atts.length() > 0); + System.out.println("\n🎯 Selected message by " + selected.getString("sender_name")); @@ -3722,6 +3739,9 @@ public class ActionHandler { System.out.println("5. Delete"); } } + if (hasAttachments) { + System.out.println("D. Download attachment"); + } System.out.println("0. Back to message list"); System.out.print("➤ Select an action: "); @@ -3754,6 +3774,15 @@ public class ActionHandler { System.out.println("❌ You are not allowed to delete this message."); } case "0" -> {} + + case "D", "d" -> { + if (hasAttachments) { + downloadAttachmentFlow(chat, selected); + } else { + System.out.println("🚫 No attachments to download."); + } + } + default -> System.out.println("❌ Invalid option."); } @@ -3763,6 +3792,126 @@ public class ActionHandler { } } + private void downloadAttachmentFlow(ChatEntry chat, JSONObject msg) { + JSONArray atts = msg.optJSONArray("attachments"); + if (atts == null || atts.length() == 0) { + System.out.println("🚫 No attachments."); + return; + } + + int idx = 0; + if (atts.length() > 1) { + System.out.print("Which attachment [1.." + atts.length() + "]? "); + try { + String ans = scanner.nextLine().trim(); + if (!ans.isEmpty()) { + int n = Integer.parseInt(ans); + if (n >= 1 && n <= atts.length()) idx = n - 1; + } + } catch (Exception ignored) { idx = 0; } + } + + JSONObject att = atts.getJSONObject(idx); + String mediaKeyStr = att.optString("media_key", ""); + if (mediaKeyStr.isBlank()) { + System.out.println("❌ Attachment missing media_key."); + return; + } + + UUID mediaKey = UUID.fromString(mediaKeyStr); + String rawName = att.optString("file_name", mediaKey.toString()); + String fileName = sanitizeFileName(rawName); + long declaredSize = att.optLong("file_size", 0L); + + // ~/Downloads/TeleSock// + String folderName = (chat.getDisplayId() != null && !chat.getDisplayId().isBlank()) + ? chat.getDisplayId() : chat.getId().toString(); + Path saveDir = Paths.get(System.getProperty("user.home"), "Downloads", "TeleSock", folderName); + + try { Files.createDirectories(saveDir); } catch (IOException e) { + System.out.println("❌ Cannot create folder: " + saveDir + " -> " + e.getMessage()); + return; + } + + // اگر قبلاً دانلود شده (و فایل واقعاً وجود دارد) + try { + Path existing = Session.downloadsIndex.find(mediaKey); + if (existing != null) { + System.out.println("✅ Already downloaded: " + existing); + return; + } + } catch (IllegalStateException notInit) { + System.out.println("⚠️ DownloadsIndex not initialized. Call DownloadsIndex.init() after login."); + // ادامه می‌دهیم؛ فقط کش نمی‌شود. + } + + // جلوگیری از overwrite با انتخاب نام یکتا + Path target = uniquePath(saveDir, fileName); + + + // دانلود روی همان سوکت: Listener را موقتاً متوقف کن + TelegramClient.mediaBusy.set(true); + try { + Path saved = TelegramClient.getDownloader().download(mediaKey, saveDir, target.getFileName().toString()); + + long sizeToRecord = declaredSize > 0 ? declaredSize : Files.size(saved); + try { + Session.downloadsIndex.put(mediaKey, saved, sizeToRecord); + } catch (IllegalStateException notInit) { + // اگر init نشده بود، تنها کش نمی‌کنیم + } + + System.out.println("✅ Saved to: " + saved + " (" + humanSize(sizeToRecord) + ")"); + } catch (Exception ex) { + System.out.println("❌ Download failed: " + ex.getMessage()); + } finally { + TelegramClient.mediaBusy.set(false); + } + } + + // نام یکتا اگر فایل موجود است: name.png -> name (1).png + private static Path uniquePath(Path dir, String fileName) { + Path p = dir.resolve(fileName); + if (!Files.exists(p)) return p; + + String name = fileName; + String ext = ""; + int dot = fileName.lastIndexOf('.'); + if (dot > 0 && dot < fileName.length()-1) { + name = fileName.substring(0, dot); + ext = fileName.substring(dot); // includes dot + } + int i = 1; + while (true) { + Path cand = dir.resolve(String.format("%s (%d)%s", name, i, ext)); + if (!Files.exists(cand)) return cand; + i++; + } + } + + private static String sanitizeFileName(String s) { + // حذف مسیر و کاراکترهای غیرمجاز (برای ویندوز/یونیکس) + s = s.replace("\\", "/"); + if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1); + // کاراکترهای نامعتبر ویندوز: \ / : * ? " < > | + s = s.replaceAll("[\\\\/:*?\"<>|]", "_"); + // جلوگیری از parent traversal + if (s.equals(".") || s.equals("..") || s.isBlank()) s = "file"; + return s; + } + + private static String humanSize(long b) { + if (b <= 0) return "0 B"; + String[] u = {"B","KB","MB","GB","TB"}; + int i = (int) Math.floor(Math.log(b) / Math.log(1024)); + if (i < 0) i = 0; + if (i >= u.length) i = u.length - 1; + double v = b / Math.pow(1024, i); + return String.format("%.1f %s", v, u[i]); + } + + + private void editMessage(UUID messageId) { System.out.print("📝 Enter new content: "); String newContent = scanner.nextLine().trim(); @@ -4130,6 +4279,39 @@ public class ActionHandler { } + + private static String safeName(String s) { + if (s == null) return "unknown"; + s = s.replace("\\", "/"); + if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1); + s = s.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); + if (s.isEmpty() || s.equals(".") || s.equals("..")) s = "unknown"; + return s; + } + + private static String accountFolderName() { + // اولویت: username → user_id → profile_name → internal_uuid + JSONObject me = Session.currentUser; + String acc = me.optString("username", + me.optString("user_id", + me.optString("profile_name", + me.optString("internal_uuid", "me")))); + return safeName(acc); + } + + private static String chatFolderName(ChatEntry chat) { + // پرایوت: اسم طرف مقابل؛ گروه/کانال: اسم چت + String name = chat.getName(); + if (name == null || name.isBlank()) { + // fallback به displayId یا id + name = chat.getDisplayId() != null && !chat.getDisplayId().isBlank() + ? chat.getDisplayId() + : String.valueOf(chat.getId()); + } + return safeName(name); + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java b/src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java new file mode 100644 index 0000000..2c29b72 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java @@ -0,0 +1,30 @@ +// DownloadIndexRegistry.java +package org.to.telegramfinalproject.Client; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class DownloadIndexRegistry { + private static final ConcurrentHashMap INSTANCES = new ConcurrentHashMap<>(); + private static volatile boolean HOOK_REGISTERED = false; + + private DownloadIndexRegistry() {} + + public static DownloadsIndex forAccount(UUID accountId) { + registerHookOnce(); + return INSTANCES.computeIfAbsent(accountId, DownloadsIndex::new); + } + + public static void closeAccount(UUID accountId) { + DownloadsIndex idx = INSTANCES.remove(accountId); + if (idx != null) idx.saveQuietly(); + } + + private static synchronized void registerHookOnce() { + if (HOOK_REGISTERED) return; + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + for (DownloadsIndex idx : INSTANCES.values()) idx.saveQuietly(); + })); + HOOK_REGISTERED = true; + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java b/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java new file mode 100644 index 0000000..12e1944 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java @@ -0,0 +1,122 @@ +package org.to.telegramfinalproject.Client; + +import org.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public final class DownloadsIndex { + private final UUID accountId; + private final Path indexFile; + private final Map map = new ConcurrentHashMap<>(); + + public DownloadsIndex(UUID accountId) { + this.accountId = accountId; + this.indexFile = resolveIndexPath(accountId.toString()); + load(); + } + + + /** مسیر فایل ایندکس مخصوص هر اکانت */ + private static Path resolveIndexPath(String accountId) { + String os = System.getProperty("os.name", "").toLowerCase(); + String home = System.getProperty("user.home"); + + Path dir; + if (os.contains("win")) { + String appData = System.getenv("APPDATA"); + dir = (appData != null) + ? Paths.get(appData, "TeleSock") + : Paths.get(home, "AppData", "Roaming", "TeleSock"); + } else { + dir = Paths.get(home, ".telesock"); + } + try { Files.createDirectories(dir); } catch (IOException ignored) {} + return dir.resolve("downloads-index-" + accountId + ".json"); + } + + /** لود از دیسک (اگر فایل وجود داشته باشد) */ + private synchronized void load() { + map.clear(); + try { + if (!Files.exists(indexFile)) return; + String json = Files.readString(indexFile, StandardCharsets.UTF_8); + if (json == null || json.isBlank()) return; + + JSONObject root = new JSONObject(json); + JSONObject items = root.optJSONObject("items"); + if (items == null) return; + + for (String key : items.keySet()) { + JSONObject e = items.getJSONObject(key); + map.put(UUID.fromString(key), new Entry( + e.getString("path"), + e.optLong("size", 0L), + e.optLong("ts", System.currentTimeMillis()) + )); + } + } catch (Exception e) { + System.err.println("⚠️ DownloadsIndex load failed: " + e.getMessage()); + } + } + + /** ذخیره اتمیک روی دیسک */ + private synchronized void save() throws IOException { + JSONObject items = new JSONObject(); + for (Map.Entry it : map.entrySet()) { + JSONObject e = new JSONObject(); + e.put("path", it.getValue().path); + e.put("size", it.getValue().size); + e.put("ts", it.getValue().ts); + items.put(it.getKey().toString(), e); + } + byte[] data = new JSONObject().put("items", items).toString(2).getBytes(StandardCharsets.UTF_8); + + Path tmp = indexFile.resolveSibling(indexFile.getFileName() + ".tmp"); + Files.write(tmp, data); + try { + Files.move(tmp, indexFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException ex) { + Files.move(tmp, indexFile, StandardCopyOption.REPLACE_EXISTING); + } + } + + /** ذخیره‌ی بی‌سر‌وصدا */ + public void saveQuietly() { + try { save(); } catch (Exception ignored) {} + } + + /** اگر قبلاً دانلود شده و فایلش هست، مسیر را برمی‌گرداند؛ وگرنه رکورد کهنه پاک می‌شود. */ + public Path find(UUID mediaKey) { + Entry e = map.get(mediaKey); + if (e == null) return null; + Path p = Paths.get(e.path); + if (Files.exists(p)) return p; + map.remove(mediaKey); + saveQuietly(); + return null; + } + + /** بعد از دانلود موفق */ + public void put(UUID mediaKey, Path path, long size) { + map.put(mediaKey, new Entry(path.toString(), size, System.currentTimeMillis())); + saveQuietly(); + } + + /** اختیاری */ + public void remove(UUID mediaKey) { + map.remove(mediaKey); + saveQuietly(); + } + + private static final class Entry { + final String path; final long size; final long ts; + Entry(String path, long size, long ts) { + this.path = path; this.size = size; this.ts = ts; + } + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 2ddbb4a..841c227 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -11,6 +11,7 @@ import java.util.concurrent.BlockingQueue; public class IncomingMessageListener implements Runnable { private final BufferedReader in; + private volatile boolean running = true; public IncomingMessageListener(BufferedReader in) { this.in = in; @@ -21,15 +22,41 @@ public class IncomingMessageListener implements Runnable { try { System.out.println("👂 Real-Time Listener started."); - String line; - while ((line = in.readLine()) != null) { + while (running) { + // ✅ اگر دانلود/آپلود مدیا در حال انجامه، اصلاً نخون + if (TelegramClient.mediaBusy.get()) { + try { Thread.sleep(15); } catch (InterruptedException ignored) {} + continue; + } + // ✅ فقط وقتی دادهٔ متنی آماده است بخوان (بدون بلاک شدن روی readLine) + if (!in.ready()) { + try { Thread.sleep(10); } catch (InterruptedException ignored) {} + continue; + } + + String line = in.readLine(); + if (line == null) { + // socket بسته شده + break; + } + + // خط‌های خالی/سفید رو رد کن + if (line.isBlank()) continue; + + // تلاش برای پارس JSON + final JSONObject response; + try { + response = new JSONObject(line); + } catch (Exception badJson) { + // اگر به هر دلیلی خط JSON نبود (مثلاً نویز)، امن رد کن + System.out.println("⚠️ [Listener] Non-JSON line ignored: " + line); + continue; + } - JSONObject response = new JSONObject(line); System.out.println("📥 Received raw line: " + line); - - //for media + // --- Media ACK routing by message_id --- String mid = response.optString("message_id", ""); if (!mid.isEmpty()) { BlockingQueue q = TelegramClient.pendingResponses.get(mid); @@ -39,9 +66,9 @@ public class IncomingMessageListener implements Runnable { } } - //if it has reqID answer + // --- General request_id response routing --- if (response.has("request_id")) { - String requestId = response.getString("request_id"); + String requestId = response.optString("request_id", ""); System.out.println("📬 Response with request_id: " + requestId); System.out.println("📬 Full response: " + response.toString(2)); @@ -52,15 +79,12 @@ public class IncomingMessageListener implements Runnable { System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue..."); TelegramClient.responseQueue.put(response); } - continue; } - - - //if it has action check it + // --- Real-time actions --- if (response.has("action")) { - String action = response.getString("action"); + String action = response.optString("action", ""); System.out.println("🎯 [Listener] Action received: " + response.toString(2)); System.out.println("🎯 Received action: " + action); @@ -69,11 +93,12 @@ public class IncomingMessageListener implements Runnable { } else { TelegramClient.responseQueue.put(response); } - } else if (response.has("status") && response.has("message")) { - TelegramClient.responseQueue.put(response); // general answer + // General success/error + TelegramClient.responseQueue.put(response); } else { - TelegramClient.responseQueue.put(response); // fallback + // Fallback + TelegramClient.responseQueue.put(response); } } @@ -90,26 +115,30 @@ public class IncomingMessageListener implements Runnable { "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","created_private_chat" , "message_reacted" , "message_unreacted" -> 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.has("data") ? response.getJSONObject("data") : new JSONObject(); - + String action = response.optString("action", ""); + JSONObject msg = response.has("data") ? response.optJSONObject("data") : new JSONObject(); switch (action) { case "added_to_group", "added_to_channel", - "removed_from_group", "removed_from_channel", "chat_deleted","created_private_chat" -> { + "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"); - String chatId = msg.getString("chat_id"); - String chatType = msg.getString("chat_type"); - ActionHandler.requestChatInfo(chatId, chatType); + String chatId = msg.optString("chat_id", ""); + String chatType = msg.optString("chat_type", ""); + if (!chatId.isBlank() && !chatType.isBlank()) { + ActionHandler.requestChatInfo(chatId, chatType); + } if (action.equals("removed_from_group") || action.equals("removed_from_channel") || action.equals("chat_deleted")) { System.out.println("🚫 You were removed from the chat or chat was deleted. Exiting..."); @@ -119,33 +148,22 @@ public class IncomingMessageListener implements Runnable { case "chat_updated" -> { System.out.println("\n🔄 Chat info updated."); - if (msg.has("last_message_time")) { - updateLastMessageTime(msg); + updateLastMessageTime(msg); } else { new Thread(() -> { - try { - handleAdminRoleChanged(msg); - } catch (IOException e) { - e.printStackTrace(); - } + try { handleAdminRoleChanged(msg); } catch (IOException e) { e.printStackTrace(); } }).start(); } } - - case "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> { + case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" -> { System.out.println("🧩 Detected admin/owner role change. Calling handler..."); new Thread(() -> { - try { - handleAdminRoleChanged(msg); //new thread - } catch (IOException e) { - e.printStackTrace(); - } + try { handleAdminRoleChanged(msg); } catch (IOException e) { e.printStackTrace(); } }).start(); } - default -> displayRealTimeMessage(action, msg); } @@ -157,25 +175,19 @@ public class IncomingMessageListener implements Runnable { UUID chatUUID = UUID.fromString(msg.getString("chat_id")); String newTime = msg.optString("last_message_time", null); - Session.chatList.stream() - .filter(chat -> chat.getId().equals(chatUUID)) - .findFirst() + Session.chatList.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() .ifPresent(chat -> { chat.setLastMessageTime(newTime); System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); }); - Session.activeChats.stream() - .filter(chat -> chat.getId().equals(chatUUID)) - .findFirst() + Session.activeChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() .ifPresent(chat -> { chat.setLastMessageTime(newTime); System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); }); - Session.archivedChats.stream() - .filter(chat -> chat.getId().equals(chatUUID)) - .findFirst() + Session.archivedChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() .ifPresent(chat -> { chat.setLastMessageTime(newTime); System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); @@ -185,24 +197,21 @@ public class IncomingMessageListener implements Runnable { if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; if (c1.getLastMessageTime() == null) return 1; if (c2.getLastMessageTime() == null) return -1; - return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); }); Session.activeChats.sort((c1, c2) -> { if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; if (c1.getLastMessageTime() == null) return 1; if (c2.getLastMessageTime() == null) return -1; - return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); }); Session.archivedChats.sort((c1, c2) -> { if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; if (c1.getLastMessageTime() == null) return 1; if (c2.getLastMessageTime() == null) return -1; - return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); }); - - - if (Session.inChatListMenu) { ActionHandler.displayChatList(); System.out.print("Select a chat by number: "); @@ -213,12 +222,12 @@ public class IncomingMessageListener implements Runnable { } } - private void handleAdminRoleChanged(JSONObject data) throws IOException { - String chatType = data.getString("chat_type"); - String chatId = data.optString("group_id", data.optString("channel_id", data.optString("chat_id", null))); + String chatType = data.optString("chat_type", ""); + String chatId = data.optString("group_id", + data.optString("channel_id", data.optString("chat_id", ""))); - if (chatId == null) { + if (chatId.isBlank()) { System.out.println("⚠️ No valid ID found in real-time data: " + data.toString(2)); return; } @@ -226,11 +235,10 @@ public class IncomingMessageListener implements Runnable { System.out.println("\n🔄 Your admin status changed. Updating chat info..."); try { - // 1. get chat info - JSONObject chatInfoReq = new JSONObject(); - chatInfoReq.put("action", "get_chat_info"); - chatInfoReq.put("receiver_id", chatId); - chatInfoReq.put("receiver_type", chatType); + JSONObject chatInfoReq = new JSONObject() + .put("action", "get_chat_info") + .put("receiver_id", chatId) + .put("receiver_type", chatType); System.out.println("📤 Sending get_chat_info: " + chatInfoReq); JSONObject chatInfoResp = ActionHandler.sendWithResponse(chatInfoReq); JSONObject chatData = chatInfoResp.getJSONObject("data"); @@ -256,21 +264,17 @@ public class IncomingMessageListener implements Runnable { Session.currentChatEntry = chat; }); - // 2. get permission JSONObject permissionReq = new JSONObject(); if (chatType.equalsIgnoreCase("group")) { - permissionReq.put("action", "get_group_permissions"); - permissionReq.put("group_id", chatId); + permissionReq.put("action", "get_group_permissions").put("group_id", chatId); } else { - permissionReq.put("action", "get_channel_permissions"); - permissionReq.put("channel_id", chatId); + permissionReq.put("action", "get_channel_permissions").put("channel_id", chatId); } JSONObject permissionResp = ActionHandler.sendWithResponse(permissionReq); JSONObject perm = permissionResp.getJSONObject("data"); entry.ifPresent(chat -> chat.setPermissions(perm)); - // 3. set currentChatId Session.currentChatId = chatUUID.toString(); System.out.println("🧪 Checking refresh conditions..."); @@ -293,20 +297,11 @@ public class IncomingMessageListener implements Runnable { } } - - - - - - - - - private void displayRealTimeMessage(String action, JSONObject msg) { switch (action) { case "new_message" -> { String senderName = msg.optString("sender_name","Unknown"); - String content = msg.optString("content","(empty)"); + String content = msg.optString("content",""); String sendAt = msg.optString("send_at","-"); String chatId = msg.optString("receiver_id", msg.optString("chat_id","")); String kind = msg.optString("kind","plain"); @@ -325,47 +320,47 @@ public class IncomingMessageListener implements Runnable { Session.currentChatId != null && Session.currentChatId.equals(chatId); if (isInCurrentChat) { + if (content.isBlank()) content = "(no content)"; // برای مدیا بدون کپشن System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")"); } else { - System.out.println("💬 Message from " + senderName + ": " + prefix + content); + String preview = content.isBlank() ? "[media]" : content; + System.out.println("💬 Message from " + senderName + ": " + prefix + preview); 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")); + System.out.println("ID: " + msg.optString("message_id","")); + System.out.println("New Content: " + msg.optString("new_content","")); + System.out.println("Edit Time: " + msg.optString("edited_at","")); } case "message_deleted_global" -> { System.out.println("\n🗑️ Message Deleted:"); - System.out.println("Message ID: " + msg.getString("message_id")); + System.out.println("Message ID: " + msg.optString("message_id","")); } case "message_reacted", "message_unreacted" -> { - String mid = msg.getString("message_id"); - String emoji = msg.getString("emoji"); - JSONObject counts = msg.optJSONObject("counts"); + String mid = msg.optString("message_id",""); + String emoji = msg.optString("emoji",""); 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")); - System.out.println("Status: " + msg.getString("status")); + System.out.println("User: " + msg.optString("user_id","")); + System.out.println("Status: " + msg.optString("status","")); } case "blocked_by_user" -> { - System.out.println("\n⛔ You were blocked by user: " + msg.getString("blocker_id")); + System.out.println("\n⛔ You were blocked by user: " + msg.optString("blocker_id","")); } case "unblocked_by_user" -> { - System.out.println("\n✅ You were unblocked by user: " + msg.getString("unblocker_id")); + System.out.println("\n✅ You were unblocked by user: " + msg.optString("unblocker_id","")); } case "message_seen" -> { System.out.println("\n👁️ Your message was seen:"); - System.out.println("Message ID: " + msg.getString("message_id")); - System.out.println("Seen at: " + msg.getString("seen_at")); + System.out.println("Message ID: " + msg.optString("message_id","")); + System.out.println("Seen at: " + msg.optString("seen_at","")); } default -> { System.out.println("\n❓ Unknown real-time action: " + action); diff --git a/src/main/java/org/to/telegramfinalproject/Client/Session.java b/src/main/java/org/to/telegramfinalproject/Client/Session.java index a13892c..f4f9475 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/Session.java +++ b/src/main/java/org/to/telegramfinalproject/Client/Session.java @@ -28,7 +28,7 @@ public class Session { public static ChatEntry currentChatEntry = null; public static List contactEntries = new ArrayList<>(); public static boolean inContactListMenu = false; - + public static DownloadsIndex downloadsIndex = null; public static String getUserUUID() { if (currentUser.has("uuid")) return currentUser.getString("uuid"); @@ -110,4 +110,7 @@ public class Session { .filter(ChatEntry::isArchived) .toList(); } + + public void setDownloadIndex(DownloadsIndex idx){ this.downloadsIndex = idx; } + } \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java b/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java new file mode 100644 index 0000000..0a128ac --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java @@ -0,0 +1,78 @@ +package org.to.telegramfinalproject.Client; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.PrintWriter; + +public final class SocketMediaDownloader { + private static final int MAGIC_DL = 0x4D444D32; + private final PrintWriter outText; // NEW + private final DataInputStream inBin; + private final DataOutputStream outBin; + + public SocketMediaDownloader(PrintWriter outText, DataInputStream inBin, DataOutputStream outBin) { + this.outText = outText; + this.inBin = inBin; + this.outBin = outBin; + } + + + public java.nio.file.Path download(java.util.UUID mediaKey, java.nio.file.Path saveDir, String fileNameHint) throws Exception { + // 1) سوییچ مود با PrintWriter + outText.print("MEDIA_DL\n"); + outText.flush(); + + // 2) هدر باینری درخواست + org.json.JSONObject req = new org.json.JSONObject() + .put("op","download") + .put("media_key", mediaKey.toString()) + .put("offset", 0); + byte[] hb = req.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + + outBin.writeInt(MAGIC_DL); + outBin.writeInt(hb.length); + outBin.write(hb); + outBin.flush(); + + // 3) پاسخ + int magic = inBin.readInt(); + if (magic != MAGIC_DL) throw new java.io.IOException("bad magic"); + + int hlen = inBin.readInt(); + byte[] hbytes = inBin.readNBytes(hlen); + org.json.JSONObject hdr = new org.json.JSONObject(new String(hbytes, java.nio.charset.StandardCharsets.UTF_8)); + if (!"success".equalsIgnoreCase(hdr.optString("status"))) { + throw new java.io.IOException("download error: " + hdr.optString("message")); + } + + long contentLen = inBin.readLong(); + String serverName = hdr.optString("file_name", fileNameHint != null ? fileNameHint : mediaKey.toString()); + + java.nio.file.Files.createDirectories(saveDir); + java.nio.file.Path dest = uniquePath(saveDir, serverName); + + try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(dest)) { + byte[] buf = new byte[8192]; + long remain = contentLen; + while (remain > 0) { + int toRead = (int) Math.min(buf.length, remain); + int n = inBin.read(buf, 0, toRead); + if (n == -1) throw new java.io.EOFException("unexpected EOF"); + os.write(buf, 0, n); + remain -= n; + } + } + return dest; + } + + private static java.nio.file.Path uniquePath(java.nio.file.Path dir, String name) throws java.io.IOException { + java.nio.file.Path p = dir.resolve(name); + if (!java.nio.file.Files.exists(p)) return p; + String base = name, ext = ""; + int dot = name.lastIndexOf('.'); + if (dot >= 0) { base = name.substring(0, dot); ext = name.substring(dot); } + int i = 1; + while (java.nio.file.Files.exists(dir.resolve(base + " (" + i + ")" + ext))) i++; + return dir.resolve(base + " (" + i + ")" + ext); + } +} diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 464b36e..84a12ea 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -4,12 +4,14 @@ import org.json.JSONObject; import java.io.*; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Scanner; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; public class TelegramClient { private static final String SERVER_HOST = "localhost"; @@ -22,7 +24,10 @@ public class TelegramClient { public static BlockingQueue responseQueue = new LinkedBlockingQueue<>(); public static UUID loggedInUserId = null; public static final Map> pendingResponses = new ConcurrentHashMap<>(); - + private DataInputStream inBin; // NEW + private static SocketMediaDownloader downloader; // NEW + public static final AtomicBoolean mediaBusy = new AtomicBoolean(false); // + private DownloadsIndex downloadIndex; private static TelegramClient instance; @@ -31,6 +36,10 @@ public class TelegramClient { instance = this; } + public static SocketMediaDownloader getDownloader() { + return downloader; + } + public static TelegramClient getInstance() { return instance; } @@ -39,9 +48,14 @@ public class TelegramClient { public void start() { try { socket = new Socket(SERVER_HOST, SERVER_PORT); - in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - out = new PrintWriter(socket.getOutputStream(), true); - outBin = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); // برای MEDIA + InputStream rawIn = socket.getInputStream(); + OutputStream rawOut = socket.getOutputStream(); + in = new BufferedReader(new InputStreamReader(rawIn, StandardCharsets.UTF_8)); + out = new PrintWriter(new OutputStreamWriter(rawOut, StandardCharsets.UTF_8), true); + + inBin = new DataInputStream(rawIn); + outBin = new DataOutputStream(rawOut); + downloader = new SocketMediaDownloader(out, inBin, outBin); System.out.println("✅ Connected to Telegram Server"); handler = new ActionHandler(out, in, outBin, scanner); @@ -78,7 +92,7 @@ public class TelegramClient { UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid")); loggedInUserId = internalId; - + this.downloadIndex = DownloadIndexRegistry.forAccount(internalId); handler.userMenu(internalId); } else { System.out.println("❌ Login failed."); @@ -120,3 +134,4 @@ public class TelegramClient { } } + diff --git a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java index 6804ad4..4429721 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/ChannelDatabase.java @@ -195,7 +195,7 @@ public class ChannelDatabase { """; try (Connection conn = ConnectionDb.connect()) { - // مرحله اول: ساخت کانال + PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, channel.getChannel_id()); stmt.setString(2, channel.getChannel_name()); @@ -208,9 +208,8 @@ public class ChannelDatabase { if (!rs.next()) return false; UUID internalUUID = (UUID) rs.getObject("internal_uuid"); - channel.setInternal_uuid(internalUUID); // اختیاری برای پیگیری بعدی + channel.setInternal_uuid(internalUUID); - // مرحله دوم: افزودن کاربر به لیست سابسکرایبرها PreparedStatement subStmt = conn.prepareStatement(subscriberSql); subStmt.setObject(1, internalUUID); subStmt.setObject(2, creatorId); diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 90ff0fc..4de0c20 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -1,14 +1,12 @@ package org.to.telegramfinalproject.Database; import org.to.telegramfinalproject.Models.FileAttachment; +import org.to.telegramfinalproject.Models.MediaRow; 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.*; import java.util.stream.Collectors; public class MessageDatabase { @@ -1197,4 +1195,171 @@ public class MessageDatabase { } } + +// +// public static MediaRow findMediaByKey(UUID mediaKey) throws SQLException { +// final String sql = """ +// SELECT a.message_id, a.storage_path, a.file_name, a.mime_type, a.file_size, +// m.receiver_type, m.receiver_id, m.sender_id +// FROM message_attachments a +// JOIN messages m ON m.message_id = a.message_id +// WHERE a.media_key = ? +// """; +// try (Connection c = ConnectionDb.connect(); +// PreparedStatement ps = c.prepareStatement(sql)) { +// ps.setObject(1, mediaKey); +// try (ResultSet rs = ps.executeQuery()) { +// if (!rs.next()) return null; +// MediaRow mr = new MediaRow(); +// mr.messageId = (UUID) rs.getObject(1); +// mr.storagePath = rs.getString(2); +// mr.fileName = rs.getString(3); +// mr.mimeType = rs.getString(4); +// mr.fileSize = rs.getLong(5); +// mr.receiverType= rs.getString(6); +// mr.receiverId = (UUID) rs.getObject(7); +// mr.senderId = (UUID) rs.getObject(8); +// return mr; +// } +// } +// } + +// public static boolean canAccess(UUID requester, MediaRow mr) { +// if ("private".equals(mr.receiverType)) { +// return requester.equals(mr.senderId) || requester.equals(mr.receiverId); +// } else if ("group".equals(mr.receiverType)) { +// return GroupDatabase.isMember(mr.receiverId, requester); +// } else if ("channel".equals(mr.receiverType)) { +// return ChannelDatabase.isUserInChannel(mr.receiverId, requester); +// } +// return false; +// } + + public static Map> findAttachmentsForMessages(List ids) throws SQLException { + Map> map = new java.util.HashMap<>(); + if (ids == null || ids.isEmpty()) return map; + + // ساخت IN به‌صورت امن + String placeholders = ids.stream().map(x -> "?").collect(java.util.stream.Collectors.joining(",")); + String sql = """ + SELECT attachment_id, message_id, media_key, file_name, file_size, mime_type, file_type, + width, height, duration_seconds, thumbnail_url, file_url, storage_path + FROM message_attachments + WHERE message_id IN (""" + placeholders + ") ORDER BY uploaded_at ASC"; + + try (Connection c = ConnectionDb.connect(); + PreparedStatement ps = c.prepareStatement(sql)) { + int i = 1; + for (UUID id : ids) ps.setObject(i++, id); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + MediaRow a = new MediaRow(); + a.attachmentId = (UUID) rs.getObject("attachment_id"); + a.messageId = (UUID) rs.getObject("message_id"); + a.mediaKey = (UUID) rs.getObject("media_key"); + a.fileName = rs.getString("file_name"); + long sz = rs.getLong("file_size"); + a.fileSize = rs.wasNull() ? null : sz; + a.mimeType = rs.getString("mime_type"); + a.fileType = rs.getString("file_type"); + int w = rs.getInt("width"); + a.width = rs.wasNull() ? null : w; + int h = rs.getInt("height"); + a.height = rs.wasNull() ? null : h; + int d = rs.getInt("duration_seconds"); + a.durationSeconds = rs.wasNull() ? null : d; + a.thumbnailUrl = rs.getString("thumbnail_url"); + a.fileUrl = rs.getString("file_url"); + a.storagePath = rs.getString("storage_path"); + + map.computeIfAbsent(a.messageId, k -> new java.util.ArrayList<>()).add(a); + } + } + } + return map; + } + + + + + + public static MediaRow findMediaByKey(UUID mediaKey) throws SQLException { + String sql = """ + SELECT + ma.attachment_id, + ma.message_id, + ma.media_key, + ma.file_name, + ma.file_size, + ma.mime_type, + ma.file_type, + ma.width, + ma.height, + ma.duration_seconds, + ma.thumbnail_url, + ma.file_url, + ma.storage_path, + m.receiver_type, + m.receiver_id, + m.sender_id + FROM message_attachments ma + JOIN messages m ON m.message_id = ma.message_id + WHERE ma.media_key = ? + LIMIT 1 + """; + + try (Connection c = ConnectionDb.connect(); + PreparedStatement ps = c.prepareStatement(sql)) { + ps.setObject(1, mediaKey); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + + MediaRow a = new MediaRow(); + a.attachmentId = (UUID) rs.getObject("attachment_id"); + a.messageId = (UUID) rs.getObject("message_id"); + a.mediaKey = (UUID) rs.getObject("media_key"); + a.fileName = rs.getString("file_name"); + + long sz = rs.getLong("file_size"); + a.fileSize = rs.wasNull() ? null : sz; // MediaRow.fileSize = Long + + a.mimeType = rs.getString("mime_type"); + a.fileType = rs.getString("file_type"); + int w = rs.getInt("width"); a.width = rs.wasNull() ? null : w; + int h = rs.getInt("height"); a.height = rs.wasNull() ? null : h; + int d = rs.getInt("duration_seconds"); a.durationSeconds = rs.wasNull() ? null : d; + a.thumbnailUrl = rs.getString("thumbnail_url"); + a.fileUrl = rs.getString("file_url"); + a.storagePath = rs.getString("storage_path"); + a.receiverType = rs.getString("receiver_type"); + a.receiverId = (UUID) rs.getObject("receiver_id"); + a.senderId = (UUID) rs.getObject("sender_id"); + return a; + } + } + } + + + public static boolean canAccess(UUID requester, MediaRow mr) { + if (requester == null || mr == null || mr.receiverType == null) return false; + + // اختیاری: فرستنده همیشه مجاز + if (requester.equals(mr.senderId)) return true; + + switch (mr.receiverType.toLowerCase(Locale.ROOT)) { + case "private": + // receiver_id در پیام‌های private = UUID چت خصوصی + return PrivateChatDatabase.isParticipant(mr.receiverId, requester); + + case "group": + return GroupDatabase.isMember(mr.receiverId, requester); + + case "channel": + return ChannelDatabase.isUserInChannel(mr.receiverId, requester); + + default: + return false; + } + } + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java index d027a5a..65ade2d 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageReactionDatabase.java @@ -1,6 +1,7 @@ package org.to.telegramfinalproject.Database; import org.json.JSONObject; +import org.to.telegramfinalproject.Models.MediaRow; import java.sql.Connection; import java.sql.PreparedStatement; @@ -8,6 +9,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; public class MessageReactionDatabase { @@ -73,4 +75,8 @@ public class MessageReactionDatabase { return counts; // مثال: {"❤️":2,"👍":1} } + + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java index 6b2ab1f..cb56fc2 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/PrivateChatDatabase.java @@ -315,4 +315,27 @@ public class PrivateChatDatabase { return null; } + public static boolean isParticipant(java.util.UUID chatId, java.util.UUID userId) { + String sql = """ + SELECT 1 + FROM private_chat + WHERE chat_id = ? + AND (user1_id = ? OR user2_id = ?) + LIMIT 1 + """; + try (var c = ConnectionDb.connect(); + var ps = c.prepareStatement(sql)) { + ps.setObject(1, chatId); + ps.setObject(2, userId); + ps.setObject(3, userId); + try (var rs = ps.executeQuery()) { + return rs.next(); + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + } diff --git a/src/main/java/org/to/telegramfinalproject/Models/MediaRow.java b/src/main/java/org/to/telegramfinalproject/Models/MediaRow.java new file mode 100644 index 0000000..0d89c82 --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/Models/MediaRow.java @@ -0,0 +1,22 @@ +package org.to.telegramfinalproject.Models; + +import java.util.UUID; + +public class MediaRow { + public UUID messageId; + public String storagePath; + public String fileName; + public String mimeType; + public Long fileSize; + public String receiverType; + public UUID receiverId; + public UUID senderId; + public java.util.UUID attachmentId; + public java.util.UUID mediaKey; + public String fileType; // IMAGE/AUDIO/... + public Integer width; + public Integer height; + public Integer durationSeconds; //for audio only + public String thumbnailUrl; + public String fileUrl; //display link +} diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 76136db..8dce818 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -32,12 +32,31 @@ public class ClientHandler implements Runnable { UUID userId = null; try ( + +// InputStream rawIn = socket.getInputStream(); +// OutputStream rawOut = socket.getOutputStream(); +// +// BufferedReader in = new BufferedReader(new InputStreamReader(rawIn, java.nio.charset.StandardCharsets.UTF_8)); +// PrintWriter out = new PrintWriter(new OutputStreamWriter(rawOut, java.nio.charset.StandardCharsets.UTF_8), true); + +// DataInputStream dis = new DataInputStream(rawIn); +// DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(rawOut)); + // 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); //for binary headers +// BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); + // DataInputStream dis = new DataInputStream(bis); //for binary headers - PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8), true); + //PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8), true); + InputStream rawIn = socket.getInputStream(); + OutputStream rawOut = socket.getOutputStream(); + + BufferedInputStream bis = new BufferedInputStream(rawIn); + BufferedOutputStream bos = new BufferedOutputStream(rawOut); + + DataInputStream dis = new DataInputStream(bis); + DataOutputStream dos = new DataOutputStream(bos); + PrintWriter out = new PrintWriter(new OutputStreamWriter(bos, java.nio.charset.StandardCharsets.UTF_8), true); ) { @@ -45,11 +64,23 @@ public class ClientHandler implements Runnable { String inputLine; while ((inputLine = readUtf8Line(bis)) != null) { + + String line = inputLine.trim(); + if ("MEDIA".equalsIgnoreCase(inputLine.trim())) { handleMediaFrame(dis, out); continue; } + if ("MEDIA_DL".equalsIgnoreCase(line)) { + if (this.currentUser.getInternal_uuid() == null) { + sendDlErr(dos, "not authorized"); + continue; + } + handleMediaDownload(dis, dos, this.currentUser.getInternal_uuid()); + continue; + } + JSONObject requestJson = new JSONObject(inputLine); String action = requestJson.getString("action"); ResponseModel response = null; @@ -2144,6 +2175,13 @@ public class ClientHandler implements Runnable { List messages = MessageDatabase.getMessagesForChat(chatId, chatType, currentUser.getInternal_uuid(), offset, limit); + java.util.List mids = new java.util.ArrayList<>(); + for (Message m : messages) mids.add(m.getMessage_id()); + + // ⬅️ همهٔ اتچمنت‌ها را یک‌جا بگیر: message_id -> list(attachments) + java.util.Map> attMap = + MessageDatabase.findAttachmentsForMessages(mids); + JSONArray result = new JSONArray(); for (Message m : messages) { JSONObject obj = new JSONObject(); @@ -2211,6 +2249,26 @@ public class ClientHandler implements Runnable { obj.put("reactions", new JSONArray(reactions)); + JSONArray atts = new JSONArray(); + java.util.List list = attMap.getOrDefault(m.getMessage_id(), java.util.Collections.emptyList()); + for (MediaRow a : list) { + JSONObject aj = new JSONObject() + .put("media_key", a.mediaKey != null ? a.mediaKey.toString() : JSONObject.NULL) + .put("file_name", a.fileName != null ? a.fileName : JSONObject.NULL) + .put("file_size", a.fileSize != null ? a.fileSize : JSONObject.NULL) + .put("mime_type", a.mimeType != null ? a.mimeType : JSONObject.NULL) + .put("file_type", a.fileType != null ? a.fileType : JSONObject.NULL) + .put("width", a.width != null ? a.width : JSONObject.NULL) + .put("height", a.height != null ? a.height : JSONObject.NULL) + .put("duration_seconds", a.durationSeconds != null ? a.durationSeconds : JSONObject.NULL) + .put("thumbnail_url", a.thumbnailUrl != null ? a.thumbnailUrl : JSONObject.NULL) + // اختیاری/دیباگ + .put("file_url", a.fileUrl != null ? a.fileUrl : JSONObject.NULL); + atts.put(aj); + } + obj.put("attachments", atts); + + result.put(obj); } @@ -2570,7 +2628,9 @@ public class ClientHandler implements Runnable { userDatabase.updateLastSeen(userId); SessionManager.removeUser(userId); } - } finally { + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { try { if (currentUser != null) { //RealTime @@ -2863,6 +2923,75 @@ public class ClientHandler implements Runnable { } } + private static final int MAGIC_DL = 0x4D444D32; // "MDM2" + + private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) { + try { + int magic = inBin.readInt(); + if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; } + + int hlen = inBin.readInt(); + if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; } + + byte[] hb = inBin.readNBytes(hlen); + if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; } + + JSONObject hdr = new JSONObject(new String(hb, java.nio.charset.StandardCharsets.UTF_8)); + if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; } + + UUID mediaKey = UUID.fromString(hdr.getString("media_key")); + long offset = Math.max(0L, hdr.optLong("offset", 0L)); + + MediaRow mr = MessageDatabase.findMediaByKey(mediaKey); + if (mr == null) { sendDlErr(outBin, "not found"); return; } + if (!MessageDatabase.canAccess(requesterId, mr)) { sendDlErr(outBin, "not authorized"); return; } + + java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize(); + long size = java.nio.file.Files.size(path); + if (offset > size) offset = 0L; + + JSONObject ok = new JSONObject() + .put("status","success") + .put("media_key", mediaKey.toString()) + .put("file_name", mr.fileName) + .put("mime_type", mr.mimeType) + .put("file_size", size); + + byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + + outBin.writeInt(MAGIC_DL); + outBin.writeInt(okb.length); + outBin.write(okb); + outBin.writeLong(size - offset); + + try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) { + if (offset > 0) fis.skipNBytes(offset); + byte[] buf = new byte[8192]; + long remain = size - offset; + while (remain > 0) { + int n = fis.read(buf, 0, (int) Math.min(buf.length, remain)); + if (n == -1) break; + outBin.write(buf, 0, n); + remain -= n; + } + } + outBin.flush(); + + } catch (Exception e) { + e.printStackTrace(); + try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {} + } + } + + private void sendDlErr(DataOutputStream outBin, String msg) throws java.io.IOException { + JSONObject j = new JSONObject().put("status","error").put("message", msg); + byte[] b = j.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + outBin.writeInt(MAGIC_DL); + outBin.writeInt(b.length); + outBin.write(b); + outBin.writeLong(0L); + outBin.flush(); + } private static void skip(DataInputStream dis, long n) throws IOException { if (n <= 0) return; From 3412648c7f3edc2988a8f90e69bafd8725924a62 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 15 Aug 2025 00:24:55 +0330 Subject: [PATCH 10/16] Work on download files(channels) --- .../Client/ActionHandler.java | 44 +++---- .../Client/DownloadIndexRegistry.java | 2 +- .../Client/DownloadsIndex.java | 1 + .../Client/SocketMediaDownloader.java | 2 +- .../Database/MessageDatabase.java | 3 +- .../telegramfinalproject/Models/MediaRow.java | 2 + .../Server/ClientHandler.java | 107 +++++++++++++++++- .../Utils/ChannelPermissionUtil.java | 25 ++++ 8 files changed, 159 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 7d65dc0..2fef192 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -3823,43 +3823,44 @@ public class ActionHandler { String fileName = sanitizeFileName(rawName); long declaredSize = att.optLong("file_size", 0L); - // ~/Downloads/TeleSock// - String folderName = (chat.getDisplayId() != null && !chat.getDisplayId().isBlank()) - ? chat.getDisplayId() : chat.getId().toString(); - Path saveDir = Paths.get(System.getProperty("user.home"), "Downloads", "TeleSock", folderName); + // ~/Downloads/TeleSock/// + String accFolder = accountFolderName(); // ← از یوزر لاگین‌شده + String chatFolder = chatFolderName(chat); // ← برای پرایوت: اسم طرف مقابل + Path saveDir = Paths.get(System.getProperty("user.home"), + "Downloads", "TeleSock", accFolder, chatFolder); - try { Files.createDirectories(saveDir); } catch (IOException e) { + // دیباگ اختیاری + System.out.println("👤 AccountFolder = " + accFolder); + System.out.println("💬 ChatFolder = " + chatFolder); + System.out.println("📁 SaveDir = " + saveDir); + + try { Files.createDirectories(saveDir); } + catch (IOException e) { System.out.println("❌ Cannot create folder: " + saveDir + " -> " + e.getMessage()); return; } - // اگر قبلاً دانلود شده (و فایل واقعاً وجود دارد) - try { - Path existing = Session.downloadsIndex.find(mediaKey); + // اگر قبلاً دانلود شده (و فایل واقعاً وجود دارد) — ایندکس مخصوص همین اکانت + DownloadsIndex di = Session.downloadsIndex; + if (di != null) { + Path existing = di.find(mediaKey); if (existing != null) { System.out.println("✅ Already downloaded: " + existing); return; } - } catch (IllegalStateException notInit) { - System.out.println("⚠️ DownloadsIndex not initialized. Call DownloadsIndex.init() after login."); - // ادامه می‌دهیم؛ فقط کش نمی‌شود. } - // جلوگیری از overwrite با انتخاب نام یکتا + // جلوگیری از overwrite Path target = uniquePath(saveDir, fileName); - - // دانلود روی همان سوکت: Listener را موقتاً متوقف کن + // دانلود روی همان سوکت TelegramClient.mediaBusy.set(true); try { - Path saved = TelegramClient.getDownloader().download(mediaKey, saveDir, target.getFileName().toString()); + Path saved = TelegramClient.getDownloader() + .download(mediaKey, saveDir, target.getFileName().toString()); long sizeToRecord = declaredSize > 0 ? declaredSize : Files.size(saved); - try { - Session.downloadsIndex.put(mediaKey, saved, sizeToRecord); - } catch (IllegalStateException notInit) { - // اگر init نشده بود، تنها کش نمی‌کنیم - } + if (di != null) di.put(mediaKey, saved, sizeToRecord); System.out.println("✅ Saved to: " + saved + " (" + humanSize(sizeToRecord) + ")"); } catch (Exception ex) { @@ -3869,6 +3870,9 @@ public class ActionHandler { } } + + + // نام یکتا اگر فایل موجود است: name.png -> name (1).png private static Path uniquePath(Path dir, String fileName) { Path p = dir.resolve(fileName); diff --git a/src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java b/src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java index 2c29b72..f89bd8f 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java +++ b/src/main/java/org/to/telegramfinalproject/Client/DownloadIndexRegistry.java @@ -27,4 +27,4 @@ public final class DownloadIndexRegistry { })); HOOK_REGISTERED = true; } -} +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java b/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java index 12e1944..b3a0f06 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java +++ b/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java @@ -1,3 +1,4 @@ + package org.to.telegramfinalproject.Client; import org.json.JSONObject; diff --git a/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java b/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java index 0a128ac..ee72ed2 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java +++ b/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java @@ -75,4 +75,4 @@ public final class SocketMediaDownloader { while (java.nio.file.Files.exists(dir.resolve(base + " (" + i + ")" + ext))) i++; return dir.resolve(base + " (" + i + ")" + ext); } -} +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index 4de0c20..ca5011b 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -3,6 +3,7 @@ package org.to.telegramfinalproject.Database; import org.to.telegramfinalproject.Models.FileAttachment; import org.to.telegramfinalproject.Models.MediaRow; import org.to.telegramfinalproject.Models.Message; +import org.to.telegramfinalproject.Utils.ChannelPermissionUtil; import java.sql.*; import java.time.LocalDateTime; @@ -1355,7 +1356,7 @@ public class MessageDatabase { return GroupDatabase.isMember(mr.receiverId, requester); case "channel": - return ChannelDatabase.isUserInChannel(mr.receiverId, requester); + return ChannelPermissionUtil.isUserInChannel(requester, mr.receiverId); default: return false; diff --git a/src/main/java/org/to/telegramfinalproject/Models/MediaRow.java b/src/main/java/org/to/telegramfinalproject/Models/MediaRow.java index 0d89c82..0332ff3 100644 --- a/src/main/java/org/to/telegramfinalproject/Models/MediaRow.java +++ b/src/main/java/org/to/telegramfinalproject/Models/MediaRow.java @@ -19,4 +19,6 @@ public class MediaRow { public Integer durationSeconds; //for audio only public String thumbnailUrl; public String fileUrl; //display link + public String chatType; + public UUID chatId; } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 8dce818..d29953d 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -22,6 +22,15 @@ public class ClientHandler implements Runnable { private final AuthService authService = new AuthService(); private User currentUser; + // ClientHandler.java + private static void log(String msg) { + System.out.println(java.time.LocalDateTime.now() + " [ClientHandler] " + msg); + } + private static void logf(String fmt, Object... args) { + log(String.format(fmt, args)); + } + + public ClientHandler(Socket socket) { this.socket = socket; @@ -72,12 +81,17 @@ public class ClientHandler implements Runnable { continue; } + if ("MEDIA_DL".equalsIgnoreCase(line)) { - if (this.currentUser.getInternal_uuid() == null) { + UUID cu = (currentUser == null ? null : currentUser.getInternal_uuid()); + logf("MEDIA_DL received. currentUser.internal_uuid=%s", cu); + + if (cu == null) { + log("MEDIA_DL rejected: currentUser is null or no internal_uuid"); sendDlErr(dos, "not authorized"); continue; } - handleMediaDownload(dis, dos, this.currentUser.getInternal_uuid()); + handleMediaDownload(dis, dos, cu); continue; } @@ -2925,8 +2939,68 @@ public class ClientHandler implements Runnable { private static final int MAGIC_DL = 0x4D444D32; // "MDM2" +// private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) { +// try { +// int magic = inBin.readInt(); +// if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; } +// +// int hlen = inBin.readInt(); +// if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; } +// +// byte[] hb = inBin.readNBytes(hlen); +// if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; } +// +// JSONObject hdr = new JSONObject(new String(hb, java.nio.charset.StandardCharsets.UTF_8)); +// if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; } +// +// UUID mediaKey = UUID.fromString(hdr.getString("media_key")); +// long offset = Math.max(0L, hdr.optLong("offset", 0L)); +// +// MediaRow mr = MessageDatabase.findMediaByKey(mediaKey); +// if (mr == null) { sendDlErr(outBin, "not found"); return; } +// if (!MessageDatabase.canAccess(requesterId, mr)) { sendDlErr(outBin, "not authorized"); return; } +// +// java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize(); +// long size = java.nio.file.Files.size(path); +// if (offset > size) offset = 0L; +// +// JSONObject ok = new JSONObject() +// .put("status","success") +// .put("media_key", mediaKey.toString()) +// .put("file_name", mr.fileName) +// .put("mime_type", mr.mimeType) +// .put("file_size", size); +// +// byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); +// +// outBin.writeInt(MAGIC_DL); +// outBin.writeInt(okb.length); +// outBin.write(okb); +// outBin.writeLong(size - offset); +// +// try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) { +// if (offset > 0) fis.skipNBytes(offset); +// byte[] buf = new byte[8192]; +// long remain = size - offset; +// while (remain > 0) { +// int n = fis.read(buf, 0, (int) Math.min(buf.length, remain)); +// if (n == -1) break; +// outBin.write(buf, 0, n); +// remain -= n; +// } +// } +// outBin.flush(); +// +// } catch (Exception e) { +// e.printStackTrace(); +// try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {} +// } +// } + private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) { try { + logf("MEDIA_DL start. requester=%s", requesterId); + int magic = inBin.readInt(); if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; } @@ -2936,15 +3010,38 @@ public class ClientHandler implements Runnable { byte[] hb = inBin.readNBytes(hlen); if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; } - JSONObject hdr = new JSONObject(new String(hb, java.nio.charset.StandardCharsets.UTF_8)); + String hdrStr = new String(hb, java.nio.charset.StandardCharsets.UTF_8); + logf("MEDIA_DL header: %s", hdrStr); + + JSONObject hdr = new JSONObject(hdrStr); if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; } UUID mediaKey = UUID.fromString(hdr.getString("media_key")); long offset = Math.max(0L, hdr.optLong("offset", 0L)); + logf("Parsed mediaKey=%s offset=%d", mediaKey, offset); MediaRow mr = MessageDatabase.findMediaByKey(mediaKey); if (mr == null) { sendDlErr(outBin, "not found"); return; } - if (!MessageDatabase.canAccess(requesterId, mr)) { sendDlErr(outBin, "not authorized"); return; } + + logf("MediaRow: chatType=%s chatId=%s sender=%s receiver=%s storage=%s", + mr.chatType, mr.chatId, mr.senderId, mr.receiverId, mr.storagePath); + + // تست مستقیم دیتابیس (موقت برای دیباگ) + try (java.sql.Connection c = ConnectionDb.connect(); + java.sql.PreparedStatement st = c.prepareStatement( + "SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1")) { + st.setObject(1, mr.chatId, java.sql.Types.OTHER); + st.setObject(2, requesterId, java.sql.Types.OTHER); + boolean direct; + try (java.sql.ResultSet r = st.executeQuery()) { direct = r.next(); } + logf("[DL] direct channel membership ch=%s user=%s => %s", mr.chatId, requesterId, direct); + } catch (Exception e) { + logf("[DL] direct membership check ERROR: %s", e.toString()); + } + + boolean allowed = MessageDatabase.canAccess(requesterId, mr); + logf("canAccess(..) -> %s", allowed); + if (!allowed) { sendDlErr(outBin, "not authorized"); return; } java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize(); long size = java.nio.file.Files.size(path); @@ -2963,6 +3060,7 @@ public class ClientHandler implements Runnable { outBin.writeInt(okb.length); outBin.write(okb); outBin.writeLong(size - offset); + logf("Sending OK header. file=%s size=%d offset=%d", mr.fileName, size, offset); try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) { if (offset > 0) fis.skipNBytes(offset); @@ -2976,6 +3074,7 @@ public class ClientHandler implements Runnable { } } outBin.flush(); + log("MEDIA_DL done."); } catch (Exception e) { e.printStackTrace(); diff --git a/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java b/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java index 709fee2..0b82779 100644 --- a/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java +++ b/src/main/java/org/to/telegramfinalproject/Utils/ChannelPermissionUtil.java @@ -73,4 +73,29 @@ public class ChannelPermissionUtil { } return false; } + + + + public static boolean isUserInChannel(UUID userId, UUID channelId) { + final String SQL = "SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1"; + try (Connection conn = ConnectionDb.connect(); + PreparedStatement ps = conn.prepareStatement(SQL)) { + + ps.setObject(1, channelId, java.sql.Types.OTHER); // 👈 مهم برای Postgres UUID + ps.setObject(2, userId, java.sql.Types.OTHER); + + System.out.println("[SQL] isUserInChannel ch=" + channelId + " user=" + userId + + " db=" + conn.getMetaData().getURL()); + + try (ResultSet rs = ps.executeQuery()) { + boolean ok = rs.next(); + System.out.println("[SQL] isUserInChannel -> " + ok); + return ok; + } + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + } From e344facb20f650e7fe2879e3383dd6c7f3921f58 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 15 Aug 2025 00:31:26 +0330 Subject: [PATCH 11/16] Clean code --- .../Client/ActionHandler.java | 26 ++++------------- .../Client/DownloadsIndex.java | 7 ----- .../Client/SocketMediaDownloader.java | 3 -- .../Server/ClientHandler.java | 29 +++++++------------ 4 files changed, 15 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 2fef192..852b96d 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -3824,12 +3824,11 @@ public class ActionHandler { long declaredSize = att.optLong("file_size", 0L); // ~/Downloads/TeleSock/// - String accFolder = accountFolderName(); // ← از یوزر لاگین‌شده - String chatFolder = chatFolderName(chat); // ← برای پرایوت: اسم طرف مقابل + String accFolder = accountFolderName(); + String chatFolder = chatFolderName(chat); Path saveDir = Paths.get(System.getProperty("user.home"), "Downloads", "TeleSock", accFolder, chatFolder); - // دیباگ اختیاری System.out.println("👤 AccountFolder = " + accFolder); System.out.println("💬 ChatFolder = " + chatFolder); System.out.println("📁 SaveDir = " + saveDir); @@ -3840,7 +3839,6 @@ public class ActionHandler { return; } - // اگر قبلاً دانلود شده (و فایل واقعاً وجود دارد) — ایندکس مخصوص همین اکانت DownloadsIndex di = Session.downloadsIndex; if (di != null) { Path existing = di.find(mediaKey); @@ -3849,11 +3847,8 @@ public class ActionHandler { return; } } - - // جلوگیری از overwrite Path target = uniquePath(saveDir, fileName); - // دانلود روی همان سوکت TelegramClient.mediaBusy.set(true); try { Path saved = TelegramClient.getDownloader() @@ -3873,7 +3868,6 @@ public class ActionHandler { - // نام یکتا اگر فایل موجود است: name.png -> name (1).png private static Path uniquePath(Path dir, String fileName) { Path p = dir.resolve(fileName); if (!Files.exists(p)) return p; @@ -3894,12 +3888,9 @@ public class ActionHandler { } private static String sanitizeFileName(String s) { - // حذف مسیر و کاراکترهای غیرمجاز (برای ویندوز/یونیکس) s = s.replace("\\", "/"); if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1); - // کاراکترهای نامعتبر ویندوز: \ / : * ? " < > | s = s.replaceAll("[\\\\/:*?\"<>|]", "_"); - // جلوگیری از parent traversal if (s.equals(".") || s.equals("..") || s.isBlank()) s = "file"; return s; } @@ -4183,7 +4174,6 @@ public class ActionHandler { public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) { - // اعتبارسنجی ورودی فایل (اگر ورودی از یوزر میاد، قبل از ساخت File کوتیشن‌ها رو حذف کن) if (file == null) { System.out.println("❌ File is null"); return; @@ -4216,21 +4206,19 @@ public class ActionHandler { byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); long contentLen = file.length(); - // صف ACK BlockingQueue q = new LinkedBlockingQueue<>(1); TelegramClient.pendingResponses.put(messageId.toString(), q); try { - // ⛔ مهم: فقط از outBin استفاده کن تا بافرها قاطی نشن - // 1) خط سوئیچ به MEDIA + outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); outBin.flush(); - // 2) فریم باینری: magic + headerLen + header + contentLen + content + // 2) binary frame: magic + headerLen + header + contentLen + content outBin.writeInt(0x4D444D31); // "MDM1" outBin.writeInt(headerBytes.length); // headerLen (int) outBin.write(headerBytes); // header - outBin.writeLong(contentLen); // contentLen (long) ← مطمئن شو سرور هم Long می‌خونه + outBin.writeLong(contentLen); // contentLen (long) try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { byte[] buf = new byte[8192]; @@ -4241,7 +4229,6 @@ public class ActionHandler { } outBin.flush(); - // 3) منتظر ACK با تایم‌اوت (فقط یکی!) JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); if (ack == null) { System.out.println("❌ Media ACK timeout for " + messageId); @@ -4294,7 +4281,6 @@ public class ActionHandler { } private static String accountFolderName() { - // اولویت: username → user_id → profile_name → internal_uuid JSONObject me = Session.currentUser; String acc = me.optString("username", me.optString("user_id", @@ -4304,10 +4290,8 @@ public class ActionHandler { } private static String chatFolderName(ChatEntry chat) { - // پرایوت: اسم طرف مقابل؛ گروه/کانال: اسم چت String name = chat.getName(); if (name == null || name.isBlank()) { - // fallback به displayId یا id name = chat.getDisplayId() != null && !chat.getDisplayId().isBlank() ? chat.getDisplayId() : String.valueOf(chat.getId()); diff --git a/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java b/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java index b3a0f06..50bdc00 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java +++ b/src/main/java/org/to/telegramfinalproject/Client/DownloadsIndex.java @@ -22,7 +22,6 @@ public final class DownloadsIndex { } - /** مسیر فایل ایندکس مخصوص هر اکانت */ private static Path resolveIndexPath(String accountId) { String os = System.getProperty("os.name", "").toLowerCase(); String home = System.getProperty("user.home"); @@ -40,7 +39,6 @@ public final class DownloadsIndex { return dir.resolve("downloads-index-" + accountId + ".json"); } - /** لود از دیسک (اگر فایل وجود داشته باشد) */ private synchronized void load() { map.clear(); try { @@ -65,7 +63,6 @@ public final class DownloadsIndex { } } - /** ذخیره اتمیک روی دیسک */ private synchronized void save() throws IOException { JSONObject items = new JSONObject(); for (Map.Entry it : map.entrySet()) { @@ -86,12 +83,10 @@ public final class DownloadsIndex { } } - /** ذخیره‌ی بی‌سر‌وصدا */ public void saveQuietly() { try { save(); } catch (Exception ignored) {} } - /** اگر قبلاً دانلود شده و فایلش هست، مسیر را برمی‌گرداند؛ وگرنه رکورد کهنه پاک می‌شود. */ public Path find(UUID mediaKey) { Entry e = map.get(mediaKey); if (e == null) return null; @@ -102,13 +97,11 @@ public final class DownloadsIndex { return null; } - /** بعد از دانلود موفق */ public void put(UUID mediaKey, Path path, long size) { map.put(mediaKey, new Entry(path.toString(), size, System.currentTimeMillis())); saveQuietly(); } - /** اختیاری */ public void remove(UUID mediaKey) { map.remove(mediaKey); saveQuietly(); diff --git a/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java b/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java index ee72ed2..4a43d89 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java +++ b/src/main/java/org/to/telegramfinalproject/Client/SocketMediaDownloader.java @@ -18,11 +18,9 @@ public final class SocketMediaDownloader { public java.nio.file.Path download(java.util.UUID mediaKey, java.nio.file.Path saveDir, String fileNameHint) throws Exception { - // 1) سوییچ مود با PrintWriter outText.print("MEDIA_DL\n"); outText.flush(); - // 2) هدر باینری درخواست org.json.JSONObject req = new org.json.JSONObject() .put("op","download") .put("media_key", mediaKey.toString()) @@ -34,7 +32,6 @@ public final class SocketMediaDownloader { outBin.write(hb); outBin.flush(); - // 3) پاسخ int magic = inBin.readInt(); if (magic != MAGIC_DL) throw new java.io.IOException("bad magic"); diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index d29953d..bc50edd 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -2681,7 +2681,7 @@ public class ClientHandler implements Runnable { if (len > 0 && sb.charAt(len - 1) == '\r') sb.setLength(len - 1); return sb.toString(); } - sb.append((char) b); // برای کنترل‌لاین‌های ASCII/UTF-8 OK + sb.append((char) b); } } @@ -2853,7 +2853,6 @@ public class ClientHandler implements Runnable { 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".equals(messageType) ? "images" : "audios"; @@ -2865,7 +2864,6 @@ public class ClientHandler implements Runnable { 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; @@ -2881,13 +2879,13 @@ public class ClientHandler implements Runnable { long fileSize = java.nio.file.Files.size(target); - String storagePath = target.toString(); // فقط سرور استفاده کنه - String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; // اختیاری/نمایشی (HTTP لازم نیست) + String storagePath = target.toString(); + String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; String mt = messageType; // "IMAGE" یا "AUDIO" int safeWidth = ("IMAGE".equals(mt) && width != null) ? width : 0; int safeHeight = ("IMAGE".equals(mt) && height != null) ? height : 0; FileAttachment att = new FileAttachment(); - att.setFileUrl(fileUrl); // اختیاری + att.setFileUrl(fileUrl); att.setFileType(messageType); // IMAGE/AUDIO att.setFileName(fileName); att.setFileSize(fileSize); @@ -2896,8 +2894,7 @@ public class ClientHandler implements Runnable { att.setHeight(safeHeight); att.setDurationSeconds(0); att.setThumbnailUrl(null); - att.setStoragePath(storagePath); // اجباری برای سوکت - // اجازه بده insertAttachmentsTx برایش mediaKey و attachmentId بسازد + att.setStoragePath(storagePath); java.util.List atts = java.util.List.of(att); boolean ok = MessageDatabase.saveMessageWithOptionalAttachments( @@ -2914,18 +2911,17 @@ public class ClientHandler implements Runnable { if (rs.next()) mediaKey = (UUID) rs.getObject(1); } } catch (SQLException sqle) { - // در بدترین حالت بدون media_key ACK می‌دیم، ولی بهتره خطا رو لاگ کنیم sqle.printStackTrace(); } JSONObject ack = new JSONObject() .put("status", ok ? "success" : "error") .put("message_id", messageId.toString()) - .put("media_key", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) // برای دانلود سوکتی + .put("media_key", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) .put("file_name", fileName) .put("file_size", fileSize) .put("mime_type", mimeType) - .put("display_path", fileUrl); // صرفاً نمایشی + .put("display_path", fileUrl); out.println(ack.toString()); out.flush(); @@ -3026,7 +3022,6 @@ public class ClientHandler implements Runnable { logf("MediaRow: chatType=%s chatId=%s sender=%s receiver=%s storage=%s", mr.chatType, mr.chatId, mr.senderId, mr.receiverId, mr.storagePath); - // تست مستقیم دیتابیس (موقت برای دیباگ) try (java.sql.Connection c = ConnectionDb.connect(); java.sql.PreparedStatement st = c.prepareStatement( "SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1")) { @@ -3105,7 +3100,6 @@ public class ClientHandler implements Runnable { } 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(); @@ -3114,25 +3108,22 @@ public class ClientHandler implements Runnable { 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"; +// if (m.equals("video/mp4")) return ".mp4"; +// if (m.equals("video/webm")) return ".webm"; // fallback - if (m.startsWith("image/")) return ""; // بگذار بدون اکستنشن ذخیره شود + if (m.startsWith("image/")) return ""; if (m.startsWith("audio/")) return ""; if (m.startsWith("video/")) return ""; From 623ea1dc3adb7cc3aa98e6bec0794b4ab6316733 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 15 Aug 2025 15:25:16 +0330 Subject: [PATCH 12/16] Real time --- .../Client/ActionHandler.java | 130 ++++++++-------- .../Client/IncomingMessageListener.java | 147 +++++++++++++----- .../Server/ClientHandler.java | 53 +++++++ .../Server/RealTimeEventDispatcher.java | 84 +++++++++- 4 files changed, 308 insertions(+), 106 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 8d7e6c2..af89ce9 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -1627,7 +1627,7 @@ public class ActionHandler { String input = scanner.nextLine().trim(); switch (input) { - case "1" -> sendMessage(chatId, "private"); + case "1" -> sendMessageInteractive(chatId, "private"); case "2" -> { viewMessagesInChat(chat); } case "3" -> { return false; } default -> System.out.println("Invalid choice."); @@ -3543,69 +3543,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")); +// } +// +// } @@ -3895,7 +3895,7 @@ public class ActionHandler { } if(input.equalsIgnoreCase("S")){ - sendMessage(chat.getId(), chat.getType()); + sendMessageInteractive(chat.getId(), chat.getType()); } try { int index = Integer.parseInt(input); diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 841c227..000db70 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -5,6 +5,9 @@ import org.to.telegramfinalproject.Models.ChatEntry; import java.io.BufferedReader; import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.BlockingQueue; @@ -117,7 +120,7 @@ public class IncomingMessageListener implements Runnable { "removed_from_group", "removed_from_channel", "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated", "created_private_chat", - "message_reacted", "message_unreacted" -> true; + "message_reacted", "message_unreacted" , "chat_updated"-> true; default -> false; }; } @@ -170,58 +173,125 @@ public class IncomingMessageListener implements Runnable { System.out.print(">> "); } +// private void updateLastMessageTime(JSONObject msg) { +// try { +// UUID chatUUID = UUID.fromString(msg.getString("chat_id")); +// String newTime = msg.optString("last_message_time", null); +// +// Session.chatList.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() +// .ifPresent(chat -> { +// chat.setLastMessageTime(newTime); +// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); +// }); +// +// Session.activeChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() +// .ifPresent(chat -> { +// chat.setLastMessageTime(newTime); +// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); +// }); +// +// Session.archivedChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() +// .ifPresent(chat -> { +// chat.setLastMessageTime(newTime); +// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); +// }); +// +// Session.chatList.sort((c1, c2) -> { +// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; +// if (c1.getLastMessageTime() == null) return 1; +// if (c2.getLastMessageTime() == null) return -1; +// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); +// }); +// Session.activeChats.sort((c1, c2) -> { +// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; +// if (c1.getLastMessageTime() == null) return 1; +// if (c2.getLastMessageTime() == null) return -1; +// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); +// }); +// Session.archivedChats.sort((c1, c2) -> { +// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; +// if (c1.getLastMessageTime() == null) return 1; +// if (c2.getLastMessageTime() == null) return -1; +// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); +// }); +// +// if (Session.inChatListMenu) { +// ActionHandler.displayChatList(); +// System.out.print("Select a chat by number: "); +// } +// +// } catch (Exception e) { +// System.out.println("❌ Failed to update last message time: " + e.getMessage()); +// } +// } + + private void updateLastMessageTime(JSONObject msg) { try { UUID chatUUID = UUID.fromString(msg.getString("chat_id")); String newTime = msg.optString("last_message_time", null); + if (newTime == null || newTime.isBlank()) return; - Session.chatList.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() - .ifPresent(chat -> { - chat.setLastMessageTime(newTime); - System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); - }); + updateOneList(Session.chatList, chatUUID, newTime); + updateOneList(Session.activeChats, chatUUID, newTime); + updateOneList(Session.archivedChats, chatUUID, newTime); - Session.activeChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() - .ifPresent(chat -> { - chat.setLastMessageTime(newTime); - System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); - }); - - Session.archivedChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() - .ifPresent(chat -> { - chat.setLastMessageTime(newTime); - System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); - }); - - Session.chatList.sort((c1, c2) -> { - if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; - if (c1.getLastMessageTime() == null) return 1; - if (c2.getLastMessageTime() == null) return -1; - return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); - }); - Session.activeChats.sort((c1, c2) -> { - if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; - if (c1.getLastMessageTime() == null) return 1; - if (c2.getLastMessageTime() == null) return -1; - return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); - }); - Session.archivedChats.sort((c1, c2) -> { - if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; - if (c1.getLastMessageTime() == null) return 1; - if (c2.getLastMessageTime() == null) return -1; - return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); - }); + sortByLastMessageTime(Session.chatList); + sortByLastMessageTime(Session.activeChats); + sortByLastMessageTime(Session.archivedChats); if (Session.inChatListMenu) { ActionHandler.displayChatList(); System.out.print("Select a chat by number: "); } - } catch (Exception e) { System.out.println("❌ Failed to update last message time: " + e.getMessage()); } } + private void updateOneList(List list, UUID chatUUID, String newTime) { + if (list == null) return; + for (ChatEntry chat : list) { + if (chatUUID.equals(chat.getId())) { // ✅ internal UUID + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + break; + } + } + } + + private static void sortByLastMessageTime(java.util.List list) { + if (list == null) return; + list.sort((a, b) -> { + var ta = parseTs(String.valueOf(a.getLastMessageTime())); + var tb = parseTs(String.valueOf(b.getLastMessageTime())); + if (ta == null && tb == null) return 0; + if (ta == null) return 1; // nullها برن آخر + if (tb == null) return -1; + return tb.compareTo(ta); // نزولی (جدیدتر بالاتر) + }); + + // اگر «Saved Messages» رو می‌خوای همیشه بالا پین کنی، این ۴ خط رو نگه دار؛ + // اگر نه، حذفش کن. + for (int i = 0; i < list.size(); i++) { + if (list.get(i).isSavedMessages()) { + list.add(0, list.remove(i)); + break; + } + } + } + + private static java.time.LocalDateTime parseTs(String s) { + if (s == null) return null; + s = s.trim(); + if (s.isEmpty() || s.equalsIgnoreCase("null")) return null; + try { return java.time.OffsetDateTime.parse(s).toLocalDateTime(); } catch (Exception ignore) {} + try { return java.time.LocalDateTime.parse(s, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (Exception ignore) {} + return null; + } + + + private void handleAdminRoleChanged(JSONObject data) throws IOException { String chatType = data.optString("chat_type", ""); String chatId = data.optString("group_id", @@ -368,4 +438,7 @@ public class IncomingMessageListener implements Runnable { } } } + + + } diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 3037223..2cf2c98 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -3046,6 +3046,59 @@ public class ClientHandler implements Runnable { out.println(ack.toString()); out.flush(); + + // بعد از out.flush(); و فقط اگر ok==true + if (ok) { + try { + // 1) دریافت پیام از DB تا send_at و... دقیق باشد + Message m = MessageDatabase.findById(messageId); // اگر چنین متدی نداری، با پارامترهای همین متد بساز/پر کن + + // 2) لیست دریافت‌کنندگان بر اساس نوع چت + List receivers = getReceiversForChat(receiverId, rType.toLowerCase()); + + + // 3) ساخت payload شامل اتچمنت (media) + User sender = userDatabase.findByInternalUUID(senderId); + JSONObject payload = new JSONObject() + .put("action", "new_message") + .put("data", new JSONObject() + .put("id", m.getMessage_id().toString()) + .put("chat_id", receiverId.toString()) + .put("chat_type", rType.toLowerCase()) + .put("sender_id", senderId.toString()) + .put("sender_name", sender != null ? sender.getProfile_name() : JSONObject.NULL) + .put("message_type", messageType.toLowerCase()) + .put("text", (text == null || text.isEmpty()) ? JSONObject.NULL : text) + .put("media", new JSONObject() + .put("media_id", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) + .put("file_name", fileName) + .put("mime_type", mimeType) + .put("size_bytes", fileSize) + .put("url", fileUrl) + .put("thumbnail_url", JSONObject.NULL) + .put("width", safeWidth) + .put("height", safeHeight) + .put("duration_ms", 0) + ) + .put("send_at", m.getSend_at().toString()) + .put("status", "SENT") + ); + + // 4) ارسال به همه اعضا (از جمله خودِ فرستنده اگر می‌خواهی UI آن هم یکپارچه آپدیت شود) + for (UUID uid : receivers) { + RealTimeEventDispatcher.sendToUser(uid, payload); + } + + // (اختیاری) رویداد آپدیت چت‌لیست برای sort بر اساس آخرین پیام + RealTimeEventDispatcher.notifyChatUpdated(receiverId, rType, m); + + } catch (Exception ex) { + ex.printStackTrace(); + // اگر ذخیره شد ولی Broadcast شکست خورد، می‌توانی Log کنی یا Retry سبک انجام دهی + } + } + + } catch (Exception e) { e.printStackTrace(); out.println(new JSONObject().put("status","error").put("message","exception").toString()); diff --git a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java index db8c6a7..6834bef 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java +++ b/src/main/java/org/to/telegramfinalproject/Server/RealTimeEventDispatcher.java @@ -1,10 +1,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.Database.*; import org.to.telegramfinalproject.Models.Message; import org.to.telegramfinalproject.Models.User; @@ -12,7 +9,9 @@ import java.io.IOException; import java.io.PrintWriter; import java.net.Socket; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.UUID; public class RealTimeEventDispatcher { @@ -425,4 +424,81 @@ public class RealTimeEventDispatcher { } + public static void notifyChatUpdated(UUID chatId, String chatType, Message lastMsg) { + if (chatId == null || chatType == null) return; + + final String type = chatType.toLowerCase(Locale.ROOT); + + List receivers; + switch (type) { + case "private": + receivers = PrivateChatDatabase.getMembers(chatId); + break; + + case "group": + receivers = GroupDatabase.getMemberUUIDs(chatId); + break; + case "channel": + receivers = ChannelDatabase.getSubscriberUUIDs(chatId); + break; + default: + receivers = Collections.emptyList(); + } + if (receivers == null || receivers.isEmpty()) return; + + // 2) ساخت خلاصه آخرین پیام برای نمایش در لیست چت + String senderName = null; + if (lastMsg != null && lastMsg.getSender_id() != null) { + User u = userDatabase.findByInternalUUID(lastMsg.getSender_id()); + if (u != null) senderName = u.getProfile_name(); + } + + String messageType = lastMsg != null && lastMsg.getMessage_type() != null + ? lastMsg.getMessage_type().toLowerCase(Locale.ROOT) : "text"; + + // preview ساده: برای مدیا، برچسب کوتاه؛ برای متن، کوتاه‌سازی + String preview; + if (!"text".equals(messageType)) { + switch (messageType) { + case "image": preview = "[Photo]"; break; + case "video": preview = "[Video]"; break; + case "audio": preview = "[Audio]"; break; + case "file": preview = "[File]"; break; + default: preview = "[Media]"; + } + } else { + String t = lastMsg != null ? nullToEmpty(lastMsg.getContent()) : ""; + preview = t.length() > 80 ? t.substring(0, 80) + "…" : t; + } + + String sendAt = (lastMsg != null && lastMsg.getSend_at() != null) + ? lastMsg.getSend_at().toString() + : java.time.OffsetDateTime.now().toString(); + + // 3) payload رویداد chat_updated + JSONObject payload = new JSONObject() + .put("action", "chat_updated") + .put("data", new JSONObject() + .put("chat_id", chatId.toString()) + .put("chat_type", type) + .put("last_message", new JSONObject() + .put("id", lastMsg != null ? lastMsg.getMessage_id().toString() : JSONObject.NULL) + .put("sender_id", lastMsg != null ? lastMsg.getSender_id().toString() : JSONObject.NULL) + .put("sender_name", senderName != null ? senderName : JSONObject.NULL) + .put("message_type", messageType) + .put("preview", preview) + .put("send_at", sendAt) + ) + .put("last_message_time", sendAt) + .put("update_reason", "new_message") // برای کلاینت مفید است + ); + + // 4) ارسال به همه اعضای چت + for (UUID uid : receivers) { + sendToUser(uid, payload); + } + } + + private static String nullToEmpty(String s) { return s == null ? "" : s; } + } From 7ef72d6d3ac6e7811df0d8ebc45ed6449140c512 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Fri, 15 Aug 2025 15:26:22 +0330 Subject: [PATCH 13/16] Real time --- .../Client/IncomingMessageListener.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 000db70..b793a9b 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -26,13 +26,11 @@ public class IncomingMessageListener implements Runnable { System.out.println("👂 Real-Time Listener started."); while (running) { - // ✅ اگر دانلود/آپلود مدیا در حال انجامه، اصلاً نخون if (TelegramClient.mediaBusy.get()) { try { Thread.sleep(15); } catch (InterruptedException ignored) {} continue; } - // ✅ فقط وقتی دادهٔ متنی آماده است بخوان (بدون بلاک شدن روی readLine) if (!in.ready()) { try { Thread.sleep(10); } catch (InterruptedException ignored) {} continue; @@ -40,19 +38,15 @@ public class IncomingMessageListener implements Runnable { String line = in.readLine(); if (line == null) { - // socket بسته شده break; } - // خط‌های خالی/سفید رو رد کن if (line.isBlank()) continue; - // تلاش برای پارس JSON final JSONObject response; try { response = new JSONObject(line); } catch (Exception badJson) { - // اگر به هر دلیلی خط JSON نبود (مثلاً نویز)، امن رد کن System.out.println("⚠️ [Listener] Non-JSON line ignored: " + line); continue; } @@ -266,13 +260,12 @@ public class IncomingMessageListener implements Runnable { var ta = parseTs(String.valueOf(a.getLastMessageTime())); var tb = parseTs(String.valueOf(b.getLastMessageTime())); if (ta == null && tb == null) return 0; - if (ta == null) return 1; // nullها برن آخر + if (ta == null) return 1; if (tb == null) return -1; - return tb.compareTo(ta); // نزولی (جدیدتر بالاتر) + return tb.compareTo(ta); }); - // اگر «Saved Messages» رو می‌خوای همیشه بالا پین کنی، این ۴ خط رو نگه دار؛ - // اگر نه، حذفش کن. + for (int i = 0; i < list.size(); i++) { if (list.get(i).isSavedMessages()) { list.add(0, list.remove(i)); From e62ffbc655861925205ca4dce3711dd252cc9e92 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Tue, 2 Sep 2025 18:12:49 +0330 Subject: [PATCH 14/16] fix --- .../Client/ActionHandler.java | 578 ++---------- .../Client/IncomingMessageListener.java | 290 +++--- .../Client/TelegramClient.java | 37 +- .../Database/MessageDatabase.java | 319 +------ .../Server/ClientHandler.java | 874 ++---------------- .../UI/LoginController.java | 2 +- 6 files changed, 271 insertions(+), 1829 deletions(-) diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index 6870747..a483f1e 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -2,18 +2,22 @@ package org.to.telegramfinalproject.Client; import org.json.JSONArray; import org.json.JSONObject; +import org.to.telegramfinalproject.Database.PrivateChatDatabase; +import org.to.telegramfinalproject.Database.ContactDatabase; import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.SearchRequestModel; +import org.to.telegramfinalproject.Models.SearchResultModel; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; import java.time.LocalDateTime; import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; public class ActionHandler { @@ -22,7 +26,6 @@ public class ActionHandler { private final Scanner scanner; public static volatile boolean forceExitChat = false; public static ActionHandler instance; - private final DataOutputStream outBin; //use for UI private volatile String lastStatus = "error"; // success | error @@ -54,12 +57,12 @@ public class ActionHandler { } } - public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) { + public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) { this.out = out; this.in = in; - this.outBin = outBin; this.scanner = scanner; ActionHandler.instance = this; + } public void login(String username , String password){ @@ -542,12 +545,6 @@ public class ActionHandler { case "register": Session.currentUser = response.getJSONObject("data"); - //for downloaded medias - UUID accountId = UUID.fromString(Session.currentUser.getString("internal_uuid")); - Session.downloadsIndex = new DownloadsIndex(accountId); - - - JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); JSONArray Archived = Session.currentUser.getJSONArray("archived_chat_list"); JSONArray Active = Session.currentUser.getJSONArray("active_chat_list"); @@ -662,6 +659,7 @@ public class ActionHandler { + Session.activeChats = activeChats; Session.archivedChats = archivedChats; Session.chatList = chatList; @@ -751,7 +749,7 @@ public class ActionHandler { if (existing != null) { openChat(existing); - } else { + } else { ChatEntry preview = new ChatEntry(); preview.setId(String.valueOf(uuid)); preview.setDisplayId(selected.getString("id")); @@ -860,13 +858,13 @@ public class ActionHandler { - break; + break; case "create_group": case "create_channel": if (response.has("data")) { - JSONObject chatJson = response.getJSONObject("data"); + JSONObject chatJson = response.getJSONObject("data"); ChatEntry chat = new ChatEntry( UUID.fromString(chatJson.getString("internal_id")), @@ -881,12 +879,12 @@ public class ActionHandler { refreshChatList(); - System.out.println("✅ Created and opening chat..."); - refreshChatList(); - openChat(chat); - } + System.out.println("✅ Created and opening chat..."); + refreshChatList(); + openChat(chat); + } - break; + break; case "get_messages": JSONArray messages = response.getJSONObject("data").getJSONArray("messages"); @@ -1471,7 +1469,7 @@ public class ActionHandler { JSONObject m = messages.getJSONObject(i); String senderId = m.getString("sender_id"); String senderName = m.optString("sender_name", "Other"); - String content = m.optString("content", ""); + String content = m.getString("content"); String time = m.getString("send_at"); String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; @@ -1693,7 +1691,7 @@ public class ActionHandler { String input = scanner.nextLine().trim(); switch (input) { - case "1" -> sendMessageInteractive(chatId, "private"); + case "1" -> sendMessage(chatId, "private"); case "2" -> { viewMessagesInChat(chat); } case "3" -> { return false; } default -> System.out.println("Invalid choice."); @@ -1712,7 +1710,7 @@ public class ActionHandler { String input = scanner.nextLine().trim(); switch (input) { - case "1" -> sendMessageInteractive(chat.getId(), "private"); + case "1" -> sendMessage(chatId, "private"); case "2" -> toggleBlock(chat.getOtherUserId()); case "3" -> { deleteChat(chatId, false); return true; } case "4" -> { deleteChat(chatId, true); return true; } @@ -1808,7 +1806,7 @@ public class ActionHandler { String input = scanner.nextLine(); switch (input) { - case "1" -> sendMessageInteractive(chat.getId(), "group"); + case "1" -> sendMessage(chat.getId(), "group"); case "2" -> viewGroupMembers(chat.getId()); case "3" -> { if (isOwner || (isAdmin && perms.optBoolean("can_add_members", false))) @@ -1935,7 +1933,7 @@ public class ActionHandler { switch (input) { case "1" -> { if (isOwner || (isAdmin && perms.optBoolean("can_post", false))) { - sendMessageInteractive(chat.getId(), "channel"); + sendMessage(chat.getId(), "channel"); } else { System.out.println("❌ You don't have permission to post."); } @@ -3675,135 +3673,82 @@ 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; -// } -// } -// -// -// -// 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: "); + String content = scanner.nextLine(); -// 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() { + System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE / AUDIO): "); + 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 / AUDIO): "); + 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(); + + // URL validation + if (fileUrl.isEmpty()) { + System.out.print("URL can not be empty. Try again."); + continue; + } + + if (fileUrl.contains(" ")) { + System.out.println("URL cannot contain spaces. Try again."); + continue; + } + + if (!fileUrl.isEmpty() && !fileUrl.matches("^(http|https)://.*$")) { + System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link."); + continue; + } + + System.out.print("File Type (IMAGE / VIDEO / FILE / AUDIO): "); + String fileType = scanner.nextLine().toUpperCase(); + + Set allowedFileTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE"); + while (!allowedFileTypes.contains(fileType)) { + System.out.print("❌ Invalid type. Try again (IMAGE / VIDEO / FILE / AUDIO): "); + 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; + } + } + + 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 refreshContactList() { JSONObject req = new JSONObject(); req.put("action", "get_contact_list"); req.put("user_id", Session.currentUser.getString("user_id")); @@ -3915,18 +3860,6 @@ public class ActionHandler { replyLabel = "↪️ Reply to " + repliedSender + ": \"" + repliedContent + "\""; } - JSONArray atts = msg.optJSONArray("attachments"); - if (atts != null && atts.length() > 0) { - System.out.println(" 📎 " + atts.length() + " attachment(s)"); - for (int a = 0; a < atts.length(); a++) { - JSONObject att = atts.getJSONObject(a); - String fn = att.optString("file_name", "(unnamed)"); - long sz = att.optLong("file_size", 0); - System.out.printf(" - #%d %s (%s)\n", a + 1, fn, humanSize(sz)); - } - } - - JSONArray reactions = msg.optJSONArray("reactions"); if (reactions != null && !reactions.isEmpty()) { System.out.print(" 💬 Reactions: "); @@ -3961,7 +3894,7 @@ public class ActionHandler { } if(input.equalsIgnoreCase("S")){ - sendMessageInteractive(chat.getId(), chat.getType()); + sendMessage(chat.getId(), chat.getType()); } try { int index = Integer.parseInt(input); @@ -3976,10 +3909,6 @@ public class ActionHandler { boolean isSender = senderId.toString().equals(Session.currentUser.getString("internal_uuid")); boolean isChannel = chat.getType().equals("channel"); boolean isOwnerOrAdmin = chat.isOwner() || chat.isAdmin(); - //for media - JSONArray atts = selected.optJSONArray("attachments"); - boolean hasAttachments = (atts != null && atts.length() > 0); - System.out.println("\n🎯 Selected message by " + selected.getString("sender_name")); @@ -4005,9 +3934,6 @@ public class ActionHandler { System.out.println("5. Delete"); } } - if (hasAttachments) { - System.out.println("D. Download attachment"); - } System.out.println("0. Back to message list"); System.out.print("➤ Select an action: "); @@ -4040,15 +3966,6 @@ public class ActionHandler { System.out.println("❌ You are not allowed to delete this message."); } case "0" -> {} - - case "D", "d" -> { - if (hasAttachments) { - downloadAttachmentFlow(chat, selected); - } else { - System.out.println("🚫 No attachments to download."); - } - } - default -> System.out.println("❌ Invalid option."); } @@ -4059,122 +3976,6 @@ public class ActionHandler { } public void editMessage(UUID messageId) { - private void downloadAttachmentFlow(ChatEntry chat, JSONObject msg) { - JSONArray atts = msg.optJSONArray("attachments"); - if (atts == null || atts.length() == 0) { - System.out.println("🚫 No attachments."); - return; - } - - int idx = 0; - if (atts.length() > 1) { - System.out.print("Which attachment [1.." + atts.length() + "]? "); - try { - String ans = scanner.nextLine().trim(); - if (!ans.isEmpty()) { - int n = Integer.parseInt(ans); - if (n >= 1 && n <= atts.length()) idx = n - 1; - } - } catch (Exception ignored) { idx = 0; } - } - - JSONObject att = atts.getJSONObject(idx); - String mediaKeyStr = att.optString("media_key", ""); - if (mediaKeyStr.isBlank()) { - System.out.println("❌ Attachment missing media_key."); - return; - } - - UUID mediaKey = UUID.fromString(mediaKeyStr); - String rawName = att.optString("file_name", mediaKey.toString()); - String fileName = sanitizeFileName(rawName); - long declaredSize = att.optLong("file_size", 0L); - - // ~/Downloads/TeleSock/// - String accFolder = accountFolderName(); - String chatFolder = chatFolderName(chat); - Path saveDir = Paths.get(System.getProperty("user.home"), - "Downloads", "TeleSock", accFolder, chatFolder); - - System.out.println("👤 AccountFolder = " + accFolder); - System.out.println("💬 ChatFolder = " + chatFolder); - System.out.println("📁 SaveDir = " + saveDir); - - try { Files.createDirectories(saveDir); } - catch (IOException e) { - System.out.println("❌ Cannot create folder: " + saveDir + " -> " + e.getMessage()); - return; - } - - DownloadsIndex di = Session.downloadsIndex; - if (di != null) { - Path existing = di.find(mediaKey); - if (existing != null) { - System.out.println("✅ Already downloaded: " + existing); - return; - } - } - Path target = uniquePath(saveDir, fileName); - - TelegramClient.mediaBusy.set(true); - try { - Path saved = TelegramClient.getDownloader() - .download(mediaKey, saveDir, target.getFileName().toString()); - - long sizeToRecord = declaredSize > 0 ? declaredSize : Files.size(saved); - if (di != null) di.put(mediaKey, saved, sizeToRecord); - - System.out.println("✅ Saved to: " + saved + " (" + humanSize(sizeToRecord) + ")"); - } catch (Exception ex) { - System.out.println("❌ Download failed: " + ex.getMessage()); - } finally { - TelegramClient.mediaBusy.set(false); - } - } - - - - - private static Path uniquePath(Path dir, String fileName) { - Path p = dir.resolve(fileName); - if (!Files.exists(p)) return p; - - String name = fileName; - String ext = ""; - int dot = fileName.lastIndexOf('.'); - if (dot > 0 && dot < fileName.length()-1) { - name = fileName.substring(0, dot); - ext = fileName.substring(dot); // includes dot - } - int i = 1; - while (true) { - Path cand = dir.resolve(String.format("%s (%d)%s", name, i, ext)); - if (!Files.exists(cand)) return cand; - i++; - } - } - - private static String sanitizeFileName(String s) { - s = s.replace("\\", "/"); - if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1); - s = s.replaceAll("[\\\\/:*?\"<>|]", "_"); - if (s.equals(".") || s.equals("..") || s.isBlank()) s = "file"; - return s; - } - - private static String humanSize(long b) { - if (b <= 0) return "0 B"; - String[] u = {"B","KB","MB","GB","TB"}; - int i = (int) Math.floor(Math.log(b) / Math.log(1024)); - if (i < 0) i = 0; - if (i >= u.length) i = u.length - 1; - double v = b / Math.pow(1024, i); - return String.format("%.1f %s", v, u[i]); - } - - - - private void editMessage(UUID messageId) { System.out.print("📝 Enter new content: "); String newContent = scanner.nextLine().trim(); @@ -4390,183 +4191,4 @@ public class ActionHandler { } - - public void sendMessageInteractive(UUID receiverId, String receiverType) { - Scanner sc = new Scanner(System.in); - - System.out.print("Type (TEXT / IMAGE / AUDIO): "); - String type = sc.nextLine().trim().toUpperCase(); - while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) { - System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): "); - type = sc.nextLine().trim().toUpperCase(); - } - - System.out.print("Text (optional for media; empty = no caption): "); - String text = sc.nextLine(); - - if ("TEXT".equals(type)) { - sendTextMessage(receiverId, receiverType, text); - } else { - System.out.print("File path: "); - String path = sc.nextLine().trim(); - File f = new File(path); - if (!f.isFile()) { - System.out.println("❌ File not found"); - return; - } - try { - sendMediaMessage(receiverId, receiverType, type, f, text); - } catch (Exception e) { - e.printStackTrace(); - System.out.println("❌ Media send failed: " + e.getMessage()); - } - } - } - - private void sendTextMessage(UUID receiverId, String receiverType, String content) { - JSONObject req = new JSONObject() - .put("action", "send_message") - .put("receiver_type", receiverType) - .put("receiver_id", receiverId.toString()) - .put("message_type", "TEXT") - .put("content", content == null ? "" : content); - - JSONObject resp = sendWithResponse(req); - if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) { - System.out.println("✅ Sent. id=" + resp.optJSONObject("data").optString("message_id","")); - } else { - System.out.println("❌ Failed: " + (resp != null ? resp.optString("message") : "no response")); - } - } - - - public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) { - if (file == null) { - System.out.println("❌ File is null"); - return; - } - if (!file.exists()) { - System.out.println("❌ File not found: " + file.getAbsolutePath()); - return; - } - if (file.isDirectory()) { - System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath()); - return; - } - - final UUID messageId = UUID.randomUUID(); - - try { - String mime = detectMime(file, type.toUpperCase()); - if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*"; - - JSONObject header = new JSONObject() - .put("message_id", messageId.toString()) - .put("sender_id", TelegramClient.loggedInUserId.toString()) - .put("receiver_type", receiverType) // private|group|channel - .put("receiver_id", receiverId.toString()) - .put("message_type", type.toUpperCase()) // IMAGE | AUDIO - .put("file_name", file.getName()) - .put("mime_type", mime) - .put("text", caption == null ? "" : caption); - - byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); - long contentLen = file.length(); - - BlockingQueue q = new LinkedBlockingQueue<>(1); - TelegramClient.pendingResponses.put(messageId.toString(), q); - - try { - - outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII)); - outBin.flush(); - - // 2) binary frame: magic + headerLen + header + contentLen + content - outBin.writeInt(0x4D444D31); // "MDM1" - outBin.writeInt(headerBytes.length); // headerLen (int) - outBin.write(headerBytes); // header - outBin.writeLong(contentLen); // contentLen (long) - - try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) { - byte[] buf = new byte[8192]; - int n; - while ((n = fis.read(buf)) != -1) { - outBin.write(buf, 0, n); - } - } - outBin.flush(); - - JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS); - if (ack == null) { - System.out.println("❌ Media ACK timeout for " + messageId); - return; - } - - String status = ack.optString("status", "error"); - if ("success".equalsIgnoreCase(status)) { - System.out.println("✅ Media sent. id=" + ack.optString("message_id") + - " url=" + ack.optString("file_url")); - } else { - System.out.println("❌ Media failed: " + ack.optString("message")); - } - - } finally { - TelegramClient.pendingResponses.remove(messageId.toString()); - } - - } catch (Exception e) { - e.printStackTrace(); - System.out.println("❌ sendMediaMessage error: " + e.getMessage()); - } - } - - - private static String detectMime(File f, String typeUpper /* IMAGE or AUDIO */) { - try { - String m = java.nio.file.Files.probeContentType(f.toPath()); - if (m != null) return m; - } catch (Exception ignored) {} - String name = f.getName().toLowerCase(); - if (name.endsWith(".png")) return "image/png"; - if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg"; - if (name.endsWith(".gif")) return "image/gif"; - if (name.endsWith(".mp3")) return "audio/mpeg"; - if (name.endsWith(".wav")) return "audio/wav"; - if (name.endsWith(".ogg")) return "audio/ogg"; - return typeUpper.equals("IMAGE") ? "image/*" : "audio/*"; - } - - - - private static String safeName(String s) { - if (s == null) return "unknown"; - s = s.replace("\\", "/"); - if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1); - s = s.replaceAll("[\\\\/:*?\"<>|]", "_").trim(); - if (s.isEmpty() || s.equals(".") || s.equals("..")) s = "unknown"; - return s; - } - - private static String accountFolderName() { - JSONObject me = Session.currentUser; - String acc = me.optString("username", - me.optString("user_id", - me.optString("profile_name", - me.optString("internal_uuid", "me")))); - return safeName(acc); - } - - private static String chatFolderName(ChatEntry chat) { - String name = chat.getName(); - if (name == null || name.isBlank()) { - name = chat.getDisplayId() != null && !chat.getDisplayId().isBlank() - ? chat.getDisplayId() - : String.valueOf(chat.getId()); - } - return safeName(name); - } - - -} - - +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java index 8f7a435..649cd9d 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java +++ b/src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java @@ -8,9 +8,6 @@ import org.to.telegramfinalproject.UI.MainController; import java.io.BufferedReader; import java.io.IOException; import java.time.LocalDateTime; -import java.util.Comparator; -import java.util.List; -import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.util.Optional; import java.util.UUID; @@ -18,7 +15,6 @@ import java.util.concurrent.BlockingQueue; public class IncomingMessageListener implements Runnable { private final BufferedReader in; - private volatile boolean running = true; public enum UIMode { CONSOLE, UI } private final UIMode uiMode; // runtime mode @@ -40,47 +36,16 @@ public class IncomingMessageListener implements Runnable { try { System.out.println("👂 Real-Time Listener started."); - while (running) { - if (TelegramClient.mediaBusy.get()) { - try { Thread.sleep(15); } catch (InterruptedException ignored) {} - continue; - } + String line; + while ((line = in.readLine()) != null) { - if (!in.ready()) { - try { Thread.sleep(10); } catch (InterruptedException ignored) {} - continue; - } - - String line = in.readLine(); - if (line == null) { - break; - } - - if (line.isBlank()) continue; - - final JSONObject response; - try { - response = new JSONObject(line); - } catch (Exception badJson) { - System.out.println("⚠️ [Listener] Non-JSON line ignored: " + line); - continue; - } + JSONObject response = new JSONObject(line); System.out.println("📥 Received raw line: " + line); - // --- Media ACK routing by message_id --- - String mid = response.optString("message_id", ""); - if (!mid.isEmpty()) { - BlockingQueue q = TelegramClient.pendingResponses.get(mid); - if (q != null) { - q.put(response); - continue; - } - } - - // --- General request_id response routing --- + //if it has reqID answer if (response.has("request_id")) { - String requestId = response.optString("request_id", ""); + String requestId = response.getString("request_id"); System.out.println("📬 Response with request_id: " + requestId); System.out.println("📬 Full response: " + response.toString(2)); @@ -91,12 +56,15 @@ public class IncomingMessageListener implements Runnable { System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue..."); TelegramClient.responseQueue.put(response); } + continue; } - // --- Real-time actions --- + + + //if it has action check it if (response.has("action")) { - String action = response.optString("action", ""); + String action = response.getString("action"); System.out.println("🎯 [Listener] Action received: " + response.toString(2)); System.out.println("🎯 Received action: " + action); @@ -105,12 +73,11 @@ public class IncomingMessageListener implements Runnable { } else { TelegramClient.responseQueue.put(response); } + } else if (response.has("status") && response.has("message")) { - // General success/error - TelegramClient.responseQueue.put(response); + TelegramClient.responseQueue.put(response); // general answer } else { - // Fallback - TelegramClient.responseQueue.put(response); + TelegramClient.responseQueue.put(response); // fallback } } @@ -127,9 +94,7 @@ public class IncomingMessageListener implements Runnable { "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", "created_private_chat", - "message_reacted", "message_unreacted" , "chat_updated"-> true; + "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted","chat_updated" -> true; default -> false; }; } @@ -148,11 +113,9 @@ public class IncomingMessageListener implements Runnable { System.out.println("🔄 Chat list changed. Updating..."); Session.forceRefreshChatList = true; - String chatId = msg.optString("chat_id", ""); - String chatType = msg.optString("chat_type", ""); - if (!chatId.isBlank() && !chatType.isBlank()) { - ActionHandler.requestChatInfo(chatId, chatType); - } + String chatId = msg.getString("chat_id"); + String chatType = msg.getString("chat_type"); + ActionHandler.requestChatInfo(chatId, chatType); if (action.equals("removed_from_group") || action.equals("removed_from_channel") || action.equals("chat_deleted")) { System.out.println("🚫 You were removed from the chat or chat was deleted. Exiting..."); @@ -227,7 +190,7 @@ public class IncomingMessageListener implements Runnable { case "message_edited", "message_deleted_global", "message_reacted", "message_unreacted" - , "blocked_by_user", "unblocked_by_user", "message_seen" -> { + , "blocked_by_user", "unblocked_by_user", "message_seen" -> { displayRealTimeMessage(action, msg); } @@ -235,136 +198,78 @@ public class IncomingMessageListener implements Runnable { System.out.println("\n❓ Unknown real-time action: " + action); System.out.println(msg.toString(2)); } - default -> displayRealTimeMessage(action, msg); } System.out.print(">> "); } -// private void updateLastMessageTime(JSONObject msg) { -// try { -// UUID chatUUID = UUID.fromString(msg.getString("chat_id")); -// String newTime = msg.optString("last_message_time", null); -// -// Session.chatList.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() -// .ifPresent(chat -> { -// chat.setLastMessageTime(newTime); -// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); -// }); -// -// Session.activeChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() -// .ifPresent(chat -> { -// chat.setLastMessageTime(newTime); -// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); -// }); -// -// Session.archivedChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst() -// .ifPresent(chat -> { -// chat.setLastMessageTime(newTime); -// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); -// }); -// -// Session.chatList.sort((c1, c2) -> { -// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; -// if (c1.getLastMessageTime() == null) return 1; -// if (c2.getLastMessageTime() == null) return -1; -// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); -// }); -// Session.activeChats.sort((c1, c2) -> { -// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; -// if (c1.getLastMessageTime() == null) return 1; -// if (c2.getLastMessageTime() == null) return -1; -// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); -// }); -// Session.archivedChats.sort((c1, c2) -> { -// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; -// if (c1.getLastMessageTime() == null) return 1; -// if (c2.getLastMessageTime() == null) return -1; -// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); -// }); -// -// if (Session.inChatListMenu) { -// ActionHandler.displayChatList(); -// System.out.print("Select a chat by number: "); -// } -// -// } catch (Exception e) { -// System.out.println("❌ Failed to update last message time: " + e.getMessage()); -// } -// } - - private void updateLastMessageTime(JSONObject msg) { try { UUID chatUUID = UUID.fromString(msg.getString("chat_id")); String newTime = msg.optString("last_message_time", null); - if (newTime == null || newTime.isBlank()) return; - updateOneList(Session.chatList, chatUUID, newTime); - updateOneList(Session.activeChats, chatUUID, newTime); - updateOneList(Session.archivedChats, chatUUID, newTime); + Session.chatList.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.activeChats.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.archivedChats.stream() + .filter(chat -> chat.getId().equals(chatUUID)) + .findFirst() + .ifPresent(chat -> { + chat.setLastMessageTime(newTime); + System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); + }); + + Session.chatList.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + Session.activeChats.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + Session.archivedChats.sort((c1, c2) -> { + if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0; + if (c1.getLastMessageTime() == null) return 1; + if (c2.getLastMessageTime() == null) return -1; + return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending + }); + + - sortByLastMessageTime(Session.chatList); - sortByLastMessageTime(Session.activeChats); - sortByLastMessageTime(Session.archivedChats); if (Session.inChatListMenu) { ActionHandler.displayChatList(); System.out.print("Select a chat by number: "); } + } catch (Exception e) { System.out.println("❌ Failed to update last message time: " + e.getMessage()); } } - private void updateOneList(List list, UUID chatUUID, String newTime) { - if (list == null) return; - for (ChatEntry chat : list) { - if (chatUUID.equals(chat.getId())) { // ✅ internal UUID - chat.setLastMessageTime(newTime); - System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId()); - break; - } - } - } - - private static void sortByLastMessageTime(java.util.List list) { - if (list == null) return; - list.sort((a, b) -> { - var ta = parseTs(String.valueOf(a.getLastMessageTime())); - var tb = parseTs(String.valueOf(b.getLastMessageTime())); - if (ta == null && tb == null) return 0; - if (ta == null) return 1; - if (tb == null) return -1; - return tb.compareTo(ta); - }); - - - for (int i = 0; i < list.size(); i++) { - if (list.get(i).isSavedMessages()) { - list.add(0, list.remove(i)); - break; - } - } - } - - private static java.time.LocalDateTime parseTs(String s) { - if (s == null) return null; - s = s.trim(); - if (s.isEmpty() || s.equalsIgnoreCase("null")) return null; - try { return java.time.OffsetDateTime.parse(s).toLocalDateTime(); } catch (Exception ignore) {} - try { return java.time.LocalDateTime.parse(s, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (Exception ignore) {} - return null; - } - - private void handleAdminRoleChanged(JSONObject data) throws IOException { - String chatType = data.optString("chat_type", ""); - String chatId = data.optString("group_id", - data.optString("channel_id", data.optString("chat_id", ""))); + String chatType = data.getString("chat_type"); + String chatId = data.optString("group_id", data.optString("channel_id", data.optString("chat_id", null))); - if (chatId.isBlank()) { + if (chatId == null) { System.out.println("⚠️ No valid ID found in real-time data: " + data.toString(2)); return; } @@ -372,10 +277,11 @@ public class IncomingMessageListener implements Runnable { System.out.println("\n🔄 Your admin status changed. Updating chat info..."); try { - JSONObject chatInfoReq = new JSONObject() - .put("action", "get_chat_info") - .put("receiver_id", chatId) - .put("receiver_type", chatType); + // 1. get chat info + JSONObject chatInfoReq = new JSONObject(); + chatInfoReq.put("action", "get_chat_info"); + chatInfoReq.put("receiver_id", chatId); + chatInfoReq.put("receiver_type", chatType); System.out.println("📤 Sending get_chat_info: " + chatInfoReq); JSONObject chatInfoResp = ActionHandler.sendWithResponse(chatInfoReq); JSONObject chatData = chatInfoResp.getJSONObject("data"); @@ -401,17 +307,21 @@ public class IncomingMessageListener implements Runnable { Session.currentChatEntry = chat; }); + // 2. get permission JSONObject permissionReq = new JSONObject(); if (chatType.equalsIgnoreCase("group")) { - permissionReq.put("action", "get_group_permissions").put("group_id", chatId); + permissionReq.put("action", "get_group_permissions"); + permissionReq.put("group_id", chatId); } else { - permissionReq.put("action", "get_channel_permissions").put("channel_id", chatId); + permissionReq.put("action", "get_channel_permissions"); + permissionReq.put("channel_id", chatId); } JSONObject permissionResp = ActionHandler.sendWithResponse(permissionReq); JSONObject perm = permissionResp.getJSONObject("data"); entry.ifPresent(chat -> chat.setPermissions(perm)); + // 3. set currentChatId Session.currentChatId = chatUUID.toString(); System.out.println("🧪 Checking refresh conditions..."); @@ -434,11 +344,20 @@ public class IncomingMessageListener implements Runnable { } } + + + + + + + + + private void displayRealTimeMessage(String action, JSONObject msg) { switch (action) { case "new_message" -> { String senderName = msg.optString("sender_name","Unknown"); - String content = msg.optString("content",""); + 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"); @@ -457,47 +376,47 @@ public class IncomingMessageListener implements Runnable { Session.currentChatId != null && Session.currentChatId.equals(chatId); if (isInCurrentChat) { - if (content.isBlank()) content = "(no content)"; // برای مدیا بدون کپشن System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")"); } else { - String preview = content.isBlank() ? "[media]" : content; - System.out.println("💬 Message from " + senderName + ": " + prefix + preview); + 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.optString("message_id","")); - System.out.println("New Content: " + msg.optString("new_content","")); - System.out.println("Edit Time: " + msg.optString("edited_at","")); + 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_global" -> { System.out.println("\n🗑️ Message Deleted:"); - System.out.println("Message ID: " + msg.optString("message_id","")); + System.out.println("Message ID: " + msg.getString("message_id")); } case "message_reacted", "message_unreacted" -> { - String mid = msg.optString("message_id",""); - String emoji = msg.optString("emoji",""); + 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.optString("user_id","")); - System.out.println("Status: " + msg.optString("status","")); + System.out.println("User: " + msg.getString("user_id")); + System.out.println("Status: " + msg.getString("status")); } case "blocked_by_user" -> { - System.out.println("\n⛔ You were blocked by user: " + msg.optString("blocker_id","")); + System.out.println("\n⛔ You were blocked by user: " + msg.getString("blocker_id")); } case "unblocked_by_user" -> { - System.out.println("\n✅ You were unblocked by user: " + msg.optString("unblocker_id","")); + System.out.println("\n✅ You were unblocked by user: " + msg.getString("unblocker_id")); } case "message_seen" -> { System.out.println("\n👁️ Your message was seen:"); - System.out.println("Message ID: " + msg.optString("message_id","")); - System.out.println("Seen at: " + msg.optString("seen_at","")); + System.out.println("Message ID: " + msg.getString("message_id")); + System.out.println("Seen at: " + msg.getString("seen_at")); } default -> { System.out.println("\n❓ Unknown real-time action: " + action); @@ -508,9 +427,6 @@ public class IncomingMessageListener implements Runnable { - - - private static LocalDateTime parseIsoFlexible(String iso) { if (iso == null || iso.isBlank()) return null; try { return LocalDateTime.parse(iso); } catch (Exception ignore) {} @@ -562,4 +478,4 @@ public class IncomingMessageListener implements Runnable { } catch (Exception e) { System.err.println("[RT] bumpChatListFromMessage: " + e.getMessage()); } } -} +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index dec1eaf..62581a1 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -128,7 +128,10 @@ package org.to.telegramfinalproject.Client; import org.json.JSONObject; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.Map; @@ -137,11 +140,9 @@ import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicBoolean; public class TelegramClient { private static final String SERVER_HOST = "localhost"; - private static final int SERVER_PORT = 8080; private static final int SERVER_PORT = 8000; private static TelegramClient instance; @@ -157,13 +158,6 @@ public class TelegramClient { public static final BlockingQueue responseQueue = new LinkedBlockingQueue<>(); public static final Map> pendingResponses = new ConcurrentHashMap<>(); public static UUID loggedInUserId = null; - public static final Map> pendingResponses = new ConcurrentHashMap<>(); - private DataInputStream inBin; // NEW - private static SocketMediaDownloader downloader; // NEW - public static final AtomicBoolean mediaBusy = new AtomicBoolean(false); // - private DownloadsIndex downloadIndex; - - private static TelegramClient instance; private volatile boolean listenerStarted = false; @@ -174,14 +168,8 @@ public class TelegramClient { public static synchronized TelegramClient getInstance() { if (instance == null) instance = new TelegramClient(); - public static SocketMediaDownloader getDownloader() { - return downloader; - } - - public static TelegramClient getInstance() { return instance; } - private DataOutputStream outBin; // public void startConsole() { // try { @@ -196,18 +184,6 @@ public class TelegramClient { public void startConsole() { try { - socket = new Socket(SERVER_HOST, SERVER_PORT); - InputStream rawIn = socket.getInputStream(); - OutputStream rawOut = socket.getOutputStream(); - in = new BufferedReader(new InputStreamReader(rawIn, StandardCharsets.UTF_8)); - out = new PrintWriter(new OutputStreamWriter(rawOut, StandardCharsets.UTF_8), true); - - inBin = new DataInputStream(rawIn); - outBin = new DataOutputStream(rawOut); - downloader = new SocketMediaDownloader(out, inBin, outBin); - - System.out.println("✅ Connected to Telegram Server"); - handler = new ActionHandler(out, in, outBin, scanner); connectIfNeeded(); initHandlerIfNeeded(); startListenerOnce(IncomingMessageListener.UIMode.CONSOLE); // ← کنسول @@ -293,7 +269,7 @@ public class TelegramClient { UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid")); loggedInUserId = internalId; - this.downloadIndex = DownloadIndexRegistry.forAccount(internalId); + handler.userMenu(internalId); } else { System.out.println("❌ Login failed."); @@ -334,5 +310,4 @@ public class TelegramClient { return listener; } -} - +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java index aa4ef9b..a377ab4 100644 --- a/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java +++ b/src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java @@ -1,9 +1,7 @@ package org.to.telegramfinalproject.Database; import org.to.telegramfinalproject.Models.FileAttachment; -import org.to.telegramfinalproject.Models.MediaRow; import org.to.telegramfinalproject.Models.Message; -import org.to.telegramfinalproject.Utils.ChannelPermissionUtil; import java.sql.*; import java.time.LocalDateTime; @@ -68,134 +66,6 @@ public class MessageDatabase { } } - - public static boolean insertMessageTx(Connection conn, UUID messageId, UUID senderId, UUID receiverId, - String receiverType, String content, String messageType) throws SQLException { - String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type) " + - "VALUES (?, ?, ?, ?, ?, ?)"; - try (PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setObject(1, messageId); - ps.setObject(2, senderId); - ps.setString(3, receiverType); - ps.setObject(4, receiverId); - if (content == null || content.isBlank()) ps.setNull(5, java.sql.Types.VARCHAR); else ps.setString(5, content); - ps.setString(6, messageType); - return ps.executeUpdate() > 0; - } - } - - public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List attachments) throws SQLException { - if (attachments == null || attachments.isEmpty()) return true; - - final String sql = """ - INSERT INTO message_attachments( - attachment_id, message_id, - file_url, file_type, file_name, file_size, mime_type, - width, height, duration_seconds, thumbnail_url, - media_key, storage_path - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) - """; - - try (PreparedStatement ps = conn.prepareStatement(sql)) { - for (FileAttachment att : attachments) { - if (att == null) throw new IllegalArgumentException("Attachment is null"); - UUID attachmentId = att.getAttachmentId() != null ? att.getAttachmentId() : UUID.randomUUID(); - UUID mediaKey = att.getMediaKey() != null ? att.getMediaKey() : attachmentId; // ساده‌ترین حالت - - String ft = att.getFileType(); - if (!"IMAGE".equalsIgnoreCase(ft) && !"AUDIO".equalsIgnoreCase(ft)) { - throw new IllegalArgumentException("file_type must be IMAGE or AUDIO"); - } - if (att.getStoragePath() == null || att.getStoragePath().isBlank()) { - throw new IllegalArgumentException("storage_path is required for socket downloads"); - } - - int i = 1; - ps.setObject(i++, attachmentId); - ps.setObject(i++, messageId); - //file url (display link) - if (att.getFileUrl() == null || att.getFileUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR); - else ps.setString(i++, att.getFileUrl()); - - ps.setString(i++, ft.toUpperCase()); - ps.setString(i++, att.getFileName()); - if (att.getFileSize() == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, att.getFileSize()); - if (att.getMimeType() == null) ps.setNull(i++, java.sql.Types.VARCHAR); else ps.setString(i++, att.getMimeType()); - if (att.getWidth() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getWidth()); - if (att.getHeight() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getHeight()); - if (att.getDurationSeconds() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getDurationSeconds()); - if (att.getThumbnailUrl() == null || att.getThumbnailUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR); - else ps.setString(i++, att.getThumbnailUrl()); - - ps.setObject(i++, mediaKey); - ps.setString(i++, att.getStoragePath()); - - ps.addBatch(); - - att.setAttachmentId(attachmentId); - att.setMediaKey(mediaKey); - } - ps.executeBatch(); - return true; - } - } - - - - public static boolean saveMessageWithOptionalAttachments( - UUID messageId, UUID senderId, UUID receiverId, - String receiverType, String content, String messageType, - List attachments - ) { - Connection conn = null; - try { - conn = ConnectionDb.connect(); - conn.setAutoCommit(false); - - boolean isText = "TEXT".equalsIgnoreCase(messageType); - boolean isImage = "IMAGE".equalsIgnoreCase(messageType); - boolean isAudio = "AUDIO".equalsIgnoreCase(messageType); - if (!isText && !isImage && !isAudio) { - throw new IllegalArgumentException("messageType must be TEXT, IMAGE, or AUDIO"); - } - - if (isText) { - if (attachments != null && !attachments.isEmpty()) - throw new IllegalArgumentException("TEXT must not have attachments"); - if (content == null || content.isBlank()) - throw new IllegalArgumentException("TEXT must have non-empty content"); - } else { - if (attachments == null || attachments.isEmpty()) - throw new IllegalArgumentException("Non-TEXT must have at least one attachment"); - - for (FileAttachment a : attachments) { - if (a == null) throw new IllegalArgumentException("Attachment is null"); - String ft = a.getFileType(); - if (isImage && !"IMAGE".equalsIgnoreCase(ft)) - throw new IllegalArgumentException("All attachments must be IMAGE for messageType=IMAGE"); - if (isAudio && !"AUDIO".equalsIgnoreCase(ft)) - throw new IllegalArgumentException("All attachments must be AUDIO for messageType=AUDIO"); - } - } - - insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType.toUpperCase()); - if (!isText) insertAttachmentsTx(conn, messageId, attachments); - - conn.commit(); - return true; - } catch (Exception e) { - if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} - e.printStackTrace(); - return false; - } finally { - if (conn != null) { - try { conn.setAutoCommit(true); } catch (SQLException ignored) {} - try { conn.close(); } catch (SQLException ignored) {} - } - } - } - - public static void markGloballyDeleted(UUID chatId) { String sql = "UPDATE messages SET is_deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'"; try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { @@ -684,34 +554,30 @@ public class MessageDatabase { public static List getAttachments(UUID messageId) { List attachments = new ArrayList<>(); - String sql = "SELECT file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url " + - "FROM message_attachments WHERE message_id = ? ORDER BY uploaded_at"; + String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?"; + try (Connection conn = ConnectionDb.connect(); PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setObject(1, messageId); ResultSet rs = stmt.executeQuery(); + while (rs.next()) { attachments.add(new FileAttachment( rs.getString("file_url"), - rs.getString("file_type"), - rs.getString("file_name"), - (Long) rs.getObject("file_size"), - rs.getString("mime_type"), - (Integer) rs.getObject("width"), - (Integer) rs.getObject("height"), - (Integer) rs.getObject("duration_seconds"), - rs.getString("thumbnail_url") + rs.getString("file_type") )); } + } catch (SQLException e) { e.printStackTrace(); } + return attachments; } - public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) { String sql = """ SELECT MAX(send_at) FROM messages @@ -779,7 +645,7 @@ public class MessageDatabase { (UUID) rs.getObject("forwarded_from"), rs.getBoolean("is_deleted_globally"), (rs.getTimestamp("edited_at") != null) ? rs.getTimestamp("edited_at").toLocalDateTime() : null - ); + ); } @@ -1297,173 +1163,6 @@ public class MessageDatabase { } -// -// public static MediaRow findMediaByKey(UUID mediaKey) throws SQLException { -// final String sql = """ -// SELECT a.message_id, a.storage_path, a.file_name, a.mime_type, a.file_size, -// m.receiver_type, m.receiver_id, m.sender_id -// FROM message_attachments a -// JOIN messages m ON m.message_id = a.message_id -// WHERE a.media_key = ? -// """; -// try (Connection c = ConnectionDb.connect(); -// PreparedStatement ps = c.prepareStatement(sql)) { -// ps.setObject(1, mediaKey); -// try (ResultSet rs = ps.executeQuery()) { -// if (!rs.next()) return null; -// MediaRow mr = new MediaRow(); -// mr.messageId = (UUID) rs.getObject(1); -// mr.storagePath = rs.getString(2); -// mr.fileName = rs.getString(3); -// mr.mimeType = rs.getString(4); -// mr.fileSize = rs.getLong(5); -// mr.receiverType= rs.getString(6); -// mr.receiverId = (UUID) rs.getObject(7); -// mr.senderId = (UUID) rs.getObject(8); -// return mr; -// } -// } -// } - -// public static boolean canAccess(UUID requester, MediaRow mr) { -// if ("private".equals(mr.receiverType)) { -// return requester.equals(mr.senderId) || requester.equals(mr.receiverId); -// } else if ("group".equals(mr.receiverType)) { -// return GroupDatabase.isMember(mr.receiverId, requester); -// } else if ("channel".equals(mr.receiverType)) { -// return ChannelDatabase.isUserInChannel(mr.receiverId, requester); -// } -// return false; -// } - - public static Map> findAttachmentsForMessages(List ids) throws SQLException { - Map> map = new java.util.HashMap<>(); - if (ids == null || ids.isEmpty()) return map; - - // ساخت IN به‌صورت امن - String placeholders = ids.stream().map(x -> "?").collect(java.util.stream.Collectors.joining(",")); - String sql = """ - SELECT attachment_id, message_id, media_key, file_name, file_size, mime_type, file_type, - width, height, duration_seconds, thumbnail_url, file_url, storage_path - FROM message_attachments - WHERE message_id IN (""" + placeholders + ") ORDER BY uploaded_at ASC"; - - try (Connection c = ConnectionDb.connect(); - PreparedStatement ps = c.prepareStatement(sql)) { - int i = 1; - for (UUID id : ids) ps.setObject(i++, id); - try (ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - MediaRow a = new MediaRow(); - a.attachmentId = (UUID) rs.getObject("attachment_id"); - a.messageId = (UUID) rs.getObject("message_id"); - a.mediaKey = (UUID) rs.getObject("media_key"); - a.fileName = rs.getString("file_name"); - long sz = rs.getLong("file_size"); - a.fileSize = rs.wasNull() ? null : sz; - a.mimeType = rs.getString("mime_type"); - a.fileType = rs.getString("file_type"); - int w = rs.getInt("width"); - a.width = rs.wasNull() ? null : w; - int h = rs.getInt("height"); - a.height = rs.wasNull() ? null : h; - int d = rs.getInt("duration_seconds"); - a.durationSeconds = rs.wasNull() ? null : d; - a.thumbnailUrl = rs.getString("thumbnail_url"); - a.fileUrl = rs.getString("file_url"); - a.storagePath = rs.getString("storage_path"); - - map.computeIfAbsent(a.messageId, k -> new java.util.ArrayList<>()).add(a); - } - } - } - return map; - } - - - - - - public static MediaRow findMediaByKey(UUID mediaKey) throws SQLException { - String sql = """ - SELECT - ma.attachment_id, - ma.message_id, - ma.media_key, - ma.file_name, - ma.file_size, - ma.mime_type, - ma.file_type, - ma.width, - ma.height, - ma.duration_seconds, - ma.thumbnail_url, - ma.file_url, - ma.storage_path, - m.receiver_type, - m.receiver_id, - m.sender_id - FROM message_attachments ma - JOIN messages m ON m.message_id = ma.message_id - WHERE ma.media_key = ? - LIMIT 1 - """; - - try (Connection c = ConnectionDb.connect(); - PreparedStatement ps = c.prepareStatement(sql)) { - ps.setObject(1, mediaKey); - try (ResultSet rs = ps.executeQuery()) { - if (!rs.next()) return null; - - MediaRow a = new MediaRow(); - a.attachmentId = (UUID) rs.getObject("attachment_id"); - a.messageId = (UUID) rs.getObject("message_id"); - a.mediaKey = (UUID) rs.getObject("media_key"); - a.fileName = rs.getString("file_name"); - - long sz = rs.getLong("file_size"); - a.fileSize = rs.wasNull() ? null : sz; // MediaRow.fileSize = Long - - a.mimeType = rs.getString("mime_type"); - a.fileType = rs.getString("file_type"); - int w = rs.getInt("width"); a.width = rs.wasNull() ? null : w; - int h = rs.getInt("height"); a.height = rs.wasNull() ? null : h; - int d = rs.getInt("duration_seconds"); a.durationSeconds = rs.wasNull() ? null : d; - a.thumbnailUrl = rs.getString("thumbnail_url"); - a.fileUrl = rs.getString("file_url"); - a.storagePath = rs.getString("storage_path"); - a.receiverType = rs.getString("receiver_type"); - a.receiverId = (UUID) rs.getObject("receiver_id"); - a.senderId = (UUID) rs.getObject("sender_id"); - return a; - } - } - } - - - public static boolean canAccess(UUID requester, MediaRow mr) { - if (requester == null || mr == null || mr.receiverType == null) return false; - - // اختیاری: فرستنده همیشه مجاز - if (requester.equals(mr.senderId)) return true; - - switch (mr.receiverType.toLowerCase(Locale.ROOT)) { - case "private": - // receiver_id در پیام‌های private = UUID چت خصوصی - return PrivateChatDatabase.isParticipant(mr.receiverId, requester); - - case "group": - return GroupDatabase.isMember(mr.receiverId, requester); - - case "channel": - return ChannelPermissionUtil.isUserInChannel(requester, mr.receiverId); - - default: - return false; - } - } - - public static Message getLastMessage(UUID targetId, String type) { final String sql = "SELECT * FROM messages " + @@ -1640,4 +1339,4 @@ public class MessageDatabase { -} +} \ No newline at end of file diff --git a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java index 21c9754..ffe0c97 100644 --- a/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java @@ -12,10 +12,6 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil; import java.io.*; import java.net.Socket; import java.sql.Connection; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.time.LocalDateTime; import java.util.*; @@ -24,15 +20,6 @@ public class ClientHandler implements Runnable { private final AuthService authService = new AuthService(); private User currentUser; - // ClientHandler.java - private static void log(String msg) { - System.out.println(java.time.LocalDateTime.now() + " [ClientHandler] " + msg); - } - private static void logf(String fmt, Object... args) { - log(String.format(fmt, args)); - } - - public ClientHandler(Socket socket) { this.socket = socket; @@ -43,60 +30,11 @@ public class ClientHandler implements Runnable { UUID userId = null; try ( - -// InputStream rawIn = socket.getInputStream(); -// OutputStream rawOut = socket.getOutputStream(); -// -// BufferedReader in = new BufferedReader(new InputStreamReader(rawIn, java.nio.charset.StandardCharsets.UTF_8)); -// PrintWriter out = new PrintWriter(new OutputStreamWriter(rawOut, java.nio.charset.StandardCharsets.UTF_8), true); - -// DataInputStream dis = new DataInputStream(rawIn); -// DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(rawOut)); - -// 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); //for binary headers - - //PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8), true); - InputStream rawIn = socket.getInputStream(); - OutputStream rawOut = socket.getOutputStream(); - - BufferedInputStream bis = new BufferedInputStream(rawIn); - BufferedOutputStream bos = new BufferedOutputStream(rawOut); - - DataInputStream dis = new DataInputStream(bis); - DataOutputStream dos = new DataOutputStream(bos); - PrintWriter out = new PrintWriter(new OutputStreamWriter(bos, java.nio.charset.StandardCharsets.UTF_8), true); - + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true) ) { - -// DataInputStream bin = new DataInputStream(new BufferedInputStream(socket.getInputStream())); - String inputLine; - while ((inputLine = readUtf8Line(bis)) != null) { - - String line = inputLine.trim(); - - if ("MEDIA".equalsIgnoreCase(inputLine.trim())) { - handleMediaFrame(dis, out); - continue; - } - - - if ("MEDIA_DL".equalsIgnoreCase(line)) { - UUID cu = (currentUser == null ? null : currentUser.getInternal_uuid()); - logf("MEDIA_DL received. currentUser.internal_uuid=%s", cu); - - if (cu == null) { - log("MEDIA_DL rejected: currentUser is null or no internal_uuid"); - sendDlErr(dos, "not authorized"); - continue; - } - handleMediaDownload(dis, dos, cu); - continue; - } - + while ((inputLine = in.readLine()) != null) { JSONObject requestJson = new JSONObject(inputLine); String action = requestJson.getString("action"); ResponseModel response = null; @@ -208,7 +146,7 @@ public class ClientHandler implements Runnable { LocalDateTime lastMessageTime = MessageDatabase.getLastMessageTime(chat.getChat_id(), "private"); - ChatEntry entry = new ChatEntry( + ChatEntry entry = new ChatEntry( chat.getChat_id(), // internal_id = chat_id isSelf ? "Saved Messages" : otherUser.getUser_id(), // id/display isSelf ? "Saved Messages" : otherUser.getProfile_name(), // name @@ -1716,7 +1654,7 @@ public class ClientHandler implements Runnable { } String chatType = requestJson.getString("chat_type"); UUID chatId = UUID.fromString(requestJson.getString("chat_id")); - userId = UUID.fromString(requestJson.getString("user_id")); + userId = UUID.fromString(requestJson.getString("user_id")); boolean success = false; @@ -1785,7 +1723,7 @@ public class ClientHandler implements Runnable { } try { UUID channelId = UUID.fromString(requestJson.getString("channel_id")); - userId = currentUser.getInternal_uuid(); + userId = currentUser.getInternal_uuid(); JSONObject permissions = ChannelDatabase.getChannelPermissions(channelId, userId); @@ -2219,7 +2157,7 @@ public class ClientHandler implements Runnable { } case "send_message" : { - response = handleSendMessage(requestJson); + response = handleSendMessage(requestJson); } break; @@ -2337,13 +2275,6 @@ public class ClientHandler implements Runnable { List messages = MessageDatabase.getMessagesForChat(chatId, chatType, currentUser.getInternal_uuid(), offset, limit); - java.util.List mids = new java.util.ArrayList<>(); - for (Message m : messages) mids.add(m.getMessage_id()); - - // ⬅️ همهٔ اتچمنت‌ها را یک‌جا بگیر: message_id -> list(attachments) - java.util.Map> attMap = - MessageDatabase.findAttachmentsForMessages(mids); - JSONArray result = new JSONArray(); for (Message m : messages) { JSONObject obj = new JSONObject(); @@ -2411,26 +2342,6 @@ public class ClientHandler implements Runnable { obj.put("reactions", new JSONArray(reactions)); - JSONArray atts = new JSONArray(); - java.util.List list = attMap.getOrDefault(m.getMessage_id(), java.util.Collections.emptyList()); - for (MediaRow a : list) { - JSONObject aj = new JSONObject() - .put("media_key", a.mediaKey != null ? a.mediaKey.toString() : JSONObject.NULL) - .put("file_name", a.fileName != null ? a.fileName : JSONObject.NULL) - .put("file_size", a.fileSize != null ? a.fileSize : JSONObject.NULL) - .put("mime_type", a.mimeType != null ? a.mimeType : JSONObject.NULL) - .put("file_type", a.fileType != null ? a.fileType : JSONObject.NULL) - .put("width", a.width != null ? a.width : JSONObject.NULL) - .put("height", a.height != null ? a.height : JSONObject.NULL) - .put("duration_seconds", a.durationSeconds != null ? a.durationSeconds : JSONObject.NULL) - .put("thumbnail_url", a.thumbnailUrl != null ? a.thumbnailUrl : JSONObject.NULL) - // اختیاری/دیباگ - .put("file_url", a.fileUrl != null ? a.fileUrl : JSONObject.NULL); - atts.put(aj); - } - obj.put("attachments", atts); - - result.put(obj); } @@ -2765,7 +2676,7 @@ public class ClientHandler implements Runnable { } case "get_blocked_users": { - userId = currentUser.getInternal_uuid(); + userId = currentUser.getInternal_uuid(); var list = ContactDatabase.getBlockedUsers(userId); org.json.JSONObject data = new org.json.JSONObject(); data.put("blocked_users", list); @@ -2774,7 +2685,7 @@ public class ClientHandler implements Runnable { } case "verify_password": { - userId = currentUser.getInternal_uuid(); + userId = currentUser.getInternal_uuid(); String cur = requestJson.getString("current_password"); User user = userDatabase.findByInternalUUID(userId); boolean ok = PasswordHashing.verify(cur, user.getPassword()); @@ -2785,7 +2696,7 @@ public class ClientHandler implements Runnable { } case "update_username": { - userId = currentUser.getInternal_uuid(); + userId = currentUser.getInternal_uuid(); String cur = requestJson.getString("current_password"); String newUsername = requestJson.getString("new_username"); boolean useBCrypt = true; @@ -2806,7 +2717,7 @@ public class ClientHandler implements Runnable { } case "update_password": { - userId = currentUser.getInternal_uuid(); + userId = currentUser.getInternal_uuid(); String cur = requestJson.getString("current_password"); String newPass = requestJson.getString("new_password"); @@ -2959,9 +2870,7 @@ public class ClientHandler implements Runnable { userDatabase.updateLastSeen(userId); SessionManager.removeUser(userId); } - } catch (SQLException e) { - throw new RuntimeException(e); - } finally { + } finally { try { if (currentUser != null) { //RealTime @@ -2986,606 +2895,9 @@ 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); - } - } - - -// 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 void handleMediaFrame(DataInputStream dis, PrintWriter out) { - try { - final int MAGIC_EXPECTED = 0x4D444D31; // "MDM1" - 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").toUpperCase(); // IMAGE | AUDIO - - if (!"IMAGE".equals(messageType) && !"AUDIO".equals(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".equals(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 storagePath = target.toString(); - String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; - String mt = messageType; // "IMAGE" یا "AUDIO" - int safeWidth = ("IMAGE".equals(mt) && width != null) ? width : 0; - int safeHeight = ("IMAGE".equals(mt) && height != null) ? height : 0; - FileAttachment att = new FileAttachment(); - att.setFileUrl(fileUrl); - att.setFileType(messageType); // IMAGE/AUDIO - att.setFileName(fileName); - att.setFileSize(fileSize); - att.setMimeType(mimeType); - att.setWidth(safeWidth); - att.setHeight(safeHeight); - att.setDurationSeconds(0); - att.setThumbnailUrl(null); - att.setStoragePath(storagePath); - java.util.List atts = java.util.List.of(att); - - boolean ok = MessageDatabase.saveMessageWithOptionalAttachments( - messageId, senderId, receiverId, rType, text, messageType, atts - ); - - UUID mediaKey = null; - try (PreparedStatement q = ConnectionDb.connect().prepareStatement( - "SELECT media_key FROM message_attachments WHERE message_id = ? AND storage_path = ? LIMIT 1" - )) { - q.setObject(1, messageId); - q.setString(2, storagePath); - try (ResultSet rs = q.executeQuery()) { - if (rs.next()) mediaKey = (UUID) rs.getObject(1); - } - } catch (SQLException sqle) { - sqle.printStackTrace(); - } - - JSONObject ack = new JSONObject() - .put("status", ok ? "success" : "error") - .put("message_id", messageId.toString()) - .put("media_key", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) - .put("file_name", fileName) - .put("file_size", fileSize) - .put("mime_type", mimeType) - .put("display_path", fileUrl); - - out.println(ack.toString()); - out.flush(); - - - // بعد از out.flush(); و فقط اگر ok==true - if (ok) { - try { - // 1) دریافت پیام از DB تا send_at و... دقیق باشد - Message m = MessageDatabase.findById(messageId); // اگر چنین متدی نداری، با پارامترهای همین متد بساز/پر کن - - // 2) لیست دریافت‌کنندگان بر اساس نوع چت - List receivers = getReceiversForChat(receiverId, rType.toLowerCase()); - - - // 3) ساخت payload شامل اتچمنت (media) - User sender = userDatabase.findByInternalUUID(senderId); - JSONObject payload = new JSONObject() - .put("action", "new_message") - .put("data", new JSONObject() - .put("id", m.getMessage_id().toString()) - .put("chat_id", receiverId.toString()) - .put("chat_type", rType.toLowerCase()) - .put("sender_id", senderId.toString()) - .put("sender_name", sender != null ? sender.getProfile_name() : JSONObject.NULL) - .put("message_type", messageType.toLowerCase()) - .put("text", (text == null || text.isEmpty()) ? JSONObject.NULL : text) - .put("media", new JSONObject() - .put("media_id", mediaKey != null ? mediaKey.toString() : JSONObject.NULL) - .put("file_name", fileName) - .put("mime_type", mimeType) - .put("size_bytes", fileSize) - .put("url", fileUrl) - .put("thumbnail_url", JSONObject.NULL) - .put("width", safeWidth) - .put("height", safeHeight) - .put("duration_ms", 0) - ) - .put("send_at", m.getSend_at().toString()) - .put("status", "SENT") - ); - - // 4) ارسال به همه اعضا (از جمله خودِ فرستنده اگر می‌خواهی UI آن هم یکپارچه آپدیت شود) - for (UUID uid : receivers) { - RealTimeEventDispatcher.sendToUser(uid, payload); - } - - // (اختیاری) رویداد آپدیت چت‌لیست برای sort بر اساس آخرین پیام - RealTimeEventDispatcher.notifyChatUpdated(receiverId, rType, m); - - } catch (Exception ex) { - ex.printStackTrace(); - // اگر ذخیره شد ولی Broadcast شکست خورد، می‌توانی Log کنی یا Retry سبک انجام دهی - } - } - - - } catch (Exception e) { - e.printStackTrace(); - out.println(new JSONObject().put("status","error").put("message","exception").toString()); - out.flush(); - } - } - - private static final int MAGIC_DL = 0x4D444D32; // "MDM2" - -// private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) { -// try { -// int magic = inBin.readInt(); -// if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; } -// -// int hlen = inBin.readInt(); -// if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; } -// -// byte[] hb = inBin.readNBytes(hlen); -// if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; } -// -// JSONObject hdr = new JSONObject(new String(hb, java.nio.charset.StandardCharsets.UTF_8)); -// if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; } -// -// UUID mediaKey = UUID.fromString(hdr.getString("media_key")); -// long offset = Math.max(0L, hdr.optLong("offset", 0L)); -// -// MediaRow mr = MessageDatabase.findMediaByKey(mediaKey); -// if (mr == null) { sendDlErr(outBin, "not found"); return; } -// if (!MessageDatabase.canAccess(requesterId, mr)) { sendDlErr(outBin, "not authorized"); return; } -// -// java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize(); -// long size = java.nio.file.Files.size(path); -// if (offset > size) offset = 0L; -// -// JSONObject ok = new JSONObject() -// .put("status","success") -// .put("media_key", mediaKey.toString()) -// .put("file_name", mr.fileName) -// .put("mime_type", mr.mimeType) -// .put("file_size", size); -// -// byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); -// -// outBin.writeInt(MAGIC_DL); -// outBin.writeInt(okb.length); -// outBin.write(okb); -// outBin.writeLong(size - offset); -// -// try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) { -// if (offset > 0) fis.skipNBytes(offset); -// byte[] buf = new byte[8192]; -// long remain = size - offset; -// while (remain > 0) { -// int n = fis.read(buf, 0, (int) Math.min(buf.length, remain)); -// if (n == -1) break; -// outBin.write(buf, 0, n); -// remain -= n; -// } -// } -// outBin.flush(); -// -// } catch (Exception e) { -// e.printStackTrace(); -// try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {} -// } -// } - - private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) { - try { - logf("MEDIA_DL start. requester=%s", requesterId); - - int magic = inBin.readInt(); - if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; } - - int hlen = inBin.readInt(); - if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; } - - byte[] hb = inBin.readNBytes(hlen); - if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; } - - String hdrStr = new String(hb, java.nio.charset.StandardCharsets.UTF_8); - logf("MEDIA_DL header: %s", hdrStr); - - JSONObject hdr = new JSONObject(hdrStr); - if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; } - - UUID mediaKey = UUID.fromString(hdr.getString("media_key")); - long offset = Math.max(0L, hdr.optLong("offset", 0L)); - logf("Parsed mediaKey=%s offset=%d", mediaKey, offset); - - MediaRow mr = MessageDatabase.findMediaByKey(mediaKey); - if (mr == null) { sendDlErr(outBin, "not found"); return; } - - logf("MediaRow: chatType=%s chatId=%s sender=%s receiver=%s storage=%s", - mr.chatType, mr.chatId, mr.senderId, mr.receiverId, mr.storagePath); - - try (java.sql.Connection c = ConnectionDb.connect(); - java.sql.PreparedStatement st = c.prepareStatement( - "SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1")) { - st.setObject(1, mr.chatId, java.sql.Types.OTHER); - st.setObject(2, requesterId, java.sql.Types.OTHER); - boolean direct; - try (java.sql.ResultSet r = st.executeQuery()) { direct = r.next(); } - logf("[DL] direct channel membership ch=%s user=%s => %s", mr.chatId, requesterId, direct); - } catch (Exception e) { - logf("[DL] direct membership check ERROR: %s", e.toString()); - } - - boolean allowed = MessageDatabase.canAccess(requesterId, mr); - logf("canAccess(..) -> %s", allowed); - if (!allowed) { sendDlErr(outBin, "not authorized"); return; } - - java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize(); - long size = java.nio.file.Files.size(path); - if (offset > size) offset = 0L; - - JSONObject ok = new JSONObject() - .put("status","success") - .put("media_key", mediaKey.toString()) - .put("file_name", mr.fileName) - .put("mime_type", mr.mimeType) - .put("file_size", size); - - byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); - - outBin.writeInt(MAGIC_DL); - outBin.writeInt(okb.length); - outBin.write(okb); - outBin.writeLong(size - offset); - logf("Sending OK header. file=%s size=%d offset=%d", mr.fileName, size, offset); - - try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) { - if (offset > 0) fis.skipNBytes(offset); - byte[] buf = new byte[8192]; - long remain = size - offset; - while (remain > 0) { - int n = fis.read(buf, 0, (int) Math.min(buf.length, remain)); - if (n == -1) break; - outBin.write(buf, 0, n); - remain -= n; - } - } - outBin.flush(); - log("MEDIA_DL done."); - - } catch (Exception e) { - e.printStackTrace(); - try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {} - } - } - - private void sendDlErr(DataOutputStream outBin, String msg) throws java.io.IOException { - JSONObject j = new JSONObject().put("status","error").put("message", msg); - byte[] b = j.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); - outBin.writeInt(MAGIC_DL); - outBin.writeInt(b.length); - outBin.write(b); - outBin.writeLong(0L); - outBin.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) { -// -// 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."); @@ -3593,71 +2905,57 @@ public class ClientHandler implements Runnable { UUID messageId = UUID.randomUUID(); UUID senderId = currentUser.getInternal_uuid(); String receiverType = json.getString("receiver_type"); - UUID receiverId = UUID.fromString(json.getString("receiver_id")); + 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"); - // Parse attachments - List attachments = new ArrayList<>(); + 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 arr = json.getJSONArray("attachments"); - for (int i = 0; i < arr.length(); i++) { - JSONObject a = arr.getJSONObject(i); + 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( - 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) + 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."); } - 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 + // 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"); @@ -3669,76 +2967,9 @@ public class ClientHandler implements Runnable { RealTimeEventDispatcher.sendToUser(senderId, chatPayload); - 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.broadcastToUsers(receivers, payload); -// -// -// -// // 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() -// .put("action", "chat_updated") -// .put("data", chatUpdate); -// -// for (UUID r : receivers) RealTimeEventDispatcher.sendToUser(r, chatPayload); - - - List allMembers = getReceiversForChat(receiverId, receiverType); // شامل sender - // به همه 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() - .put("action", "chat_updated") - .put("data", chatUpdate); - for (UUID u : allMembers) RealTimeEventDispatcher.sendToUser(u, chatPayload); - - List others = new ArrayList<>(allMembers); - others.remove(senderId); - RealTimeEventDispatcher.broadcastToUsers(others, payload); - - - JSONObject respData = new JSONObject().put("message_id", messageId.toString()); - return new ResponseModel("success", "Message sent successfully.", respData); + data.put("message_id", messageId.toString()); + return new ResponseModel("success", "Message sent successfully.", data); } catch (Exception e) { e.printStackTrace(); @@ -3747,7 +2978,6 @@ 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/UI/LoginController.java b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java index c3a5124..2e16767 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/LoginController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/LoginController.java @@ -40,7 +40,7 @@ public class LoginController { visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty()); try { - connection = new ClientConnection("localhost", 8000); + connection = new ClientConnection("localhost", 8080); } catch (Exception e) { System.out.println("Could not connect to server: " + e.getMessage()); } From 95dd69896b8d89c7a2020d2e602443b8b51f9d2b Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Wed, 3 Sep 2025 11:40:28 +0330 Subject: [PATCH 15/16] Handle send message bar in different situation --- .../Client/ActionHandler.java | 24 + .../Client/TelegramClient.java | 2 +- .../UI/ChatPageController.java | 428 ++++++++++++++++-- .../telegramfinalproject/UI/ChatViewMode.java | 12 + .../UI/MainController.java | 156 ++++++- .../org/to/telegramfinalproject/CSS/chat.css | 76 ++++ .../telegramfinalproject/Fxml/chat_page.fxml | 132 +++--- 7 files changed, 733 insertions(+), 97 deletions(-) create mode 100644 src/main/java/org/to/telegramfinalproject/UI/ChatViewMode.java create mode 100644 src/main/resources/org/to/telegramfinalproject/CSS/chat.css diff --git a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java index a483f1e..9175d22 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java +++ b/src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java @@ -446,7 +446,31 @@ public class ActionHandler { send(req); } + public void createGroupUI(String id, String name, String url, String user ){ + JSONObject req = new JSONObject(); + req.put("action", "create_group"); + req.put("user_id", user); + req.put("group_id", id); + req.put("group_name", name); + req.put("image_url", url.isBlank() ? JSONObject.NULL : url); + send(req); + + } + + + public void createChannelUI(String id, String name, String url , String user){ + + JSONObject req = new JSONObject(); + req.put("action", "create_channel"); + req.put("user_id", user); + req.put("channel_id", id); + req.put("channel_name", name); + req.put("image_url", url.isBlank() ? JSONObject.NULL : url); + + send(req); + + } public void createChannel() { String channelId = null; diff --git a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java index 62581a1..32eff13 100644 --- a/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java +++ b/src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java @@ -143,7 +143,7 @@ import java.util.concurrent.LinkedBlockingQueue; public class TelegramClient { private static final String SERVER_HOST = "localhost"; - private static final int SERVER_PORT = 8000; + private static final int SERVER_PORT = 8080; private static TelegramClient instance; diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java index 45a0000..fdcc9fb 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatPageController.java @@ -1,6 +1,7 @@ package org.to.telegramfinalproject.UI; import javafx.application.Platform; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Side; import javafx.scene.control.*; @@ -75,6 +76,25 @@ public class ChatPageController { @FXML private ImageView sendIcon; + // ===== For Searching System ===== + @FXML private VBox composerPane; + @FXML private VBox joinPane; + @FXML private VBox addContactPane; + + //For search handeling + @FXML private Button joinButton; + @FXML private Button addContactButton; + + //Handle View chat + @FXML private Button unblockBtn; + @FXML private VBox readOnlyPane; + @FXML private Label readOnlyLabel; + + + private ChatViewMode currentMode = ChatViewMode.NORMAL; + + + // ===== Time formatter for messages ===== private static final DateTimeFormatter FMT_HHMM = DateTimeFormatter.ofPattern("HH:mm"); private static final DateTimeFormatter FMT_DATE_TIME = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm"); @@ -533,39 +553,126 @@ public class ChatPageController { return new Image(url.toExternalForm()); } - public void showChat(ChatEntry entry) { +// public void showChat(ChatEntry entry) { +// this.currentChat = entry; +// +// chatTitle.setText(entry.getName()); +// +// // آواتار پیش‌فرض بر اساس نوع +// // ChatPageController.showChat(...) +// if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { +// Image img = AvatarLocalResolver.load(entry.getImageUrl()); +// if (img != null) { +// userAvatar.setImage(img); +// } else { +// // ⬇️ فال‌بک بر اساس نوع +// setDefaultHeaderAvatarByType(entry.getType()); +// } +// } else { +// setDefaultHeaderAvatarByType(entry.getType()); +// } +//// userAvatar.setClip(new Circle(20, 20, 20)); +// AvatarFX.circleClip(userAvatar, 36); +// +// +// +// fetchAndRenderHeader(entry); +// +// // پیام‌ها +// messageContainer.getChildren().clear(); +// loadMessages(entry); +// markAsRead(entry); +// +// Platform.runLater(() -> messageInput.requestFocus()); +// } + +// +// public void showChat(ChatEntry entry) { +// this.currentChat = entry; +// this.chatName = entry.getName(); // برای لاگ/منو +// +// chatTitle.setText(entry.getName()); +// if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { +// Image img = AvatarLocalResolver.load(entry.getImageUrl()); +// if (img != null) userAvatar.setImage(img); else setDefaultHeaderAvatarByType(entry.getType()); +// } else { +// setDefaultHeaderAvatarByType(entry.getType()); +// } +// AvatarFX.circleClip(userAvatar, 36); +// +// fetchAndRenderHeader(entry); +// +// messageContainer.getChildren().clear(); +// loadMessages(entry); +// markAsRead(entry); +// +// applyMode(ChatViewMode.NORMAL); +// +// Platform.runLater(() -> messageInput.requestFocus()); +// } + + +public void showChat(ChatEntry entry) { + this.currentChat = entry; + this.chatName = entry.getName(); + + chatTitle.setText(entry.getName()); + if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { + Image img = AvatarLocalResolver.load(entry.getImageUrl()); + if (img != null) userAvatar.setImage(img); else setDefaultHeaderAvatarByType(entry.getType()); + } else { + setDefaultHeaderAvatarByType(entry.getType()); + } + AvatarFX.circleClip(userAvatar, 36); + + // حالت اولیه (بدون انتظار هدر) + if ("channel".equalsIgnoreCase(entry.getType())) { + boolean canPostLocal = entry.isOwner() || entry.isAdmin() + || (entry.getPermissions()!=null && entry.getPermissions().optBoolean("can_post", false)); + applyMode(canPostLocal ? ChatViewMode.NORMAL : ChatViewMode.READ_ONLY); + } else { + applyMode(ChatViewMode.NORMAL); + } + + messageContainer.getChildren().clear(); + loadMessages(entry); + markAsRead(entry); + + // حالا هدر بیاد، دوباره نهایی‌اش می‌کنیم + fetchAndRenderHeader(entry); +} + + + + + public void showChat(ChatEntry entry, ChatViewMode mode) { this.currentChat = entry; + // --- Header --- chatTitle.setText(entry.getName()); - - // آواتار پیش‌فرض بر اساس نوع - // ChatPageController.showChat(...) if (entry.getImageUrl() != null && !entry.getImageUrl().isEmpty()) { Image img = AvatarLocalResolver.load(entry.getImageUrl()); - if (img != null) { - userAvatar.setImage(img); - } else { - // ⬇️ فال‌بک بر اساس نوع - setDefaultHeaderAvatarByType(entry.getType()); - } + if (img != null) userAvatar.setImage(img); else setDefaultHeaderAvatarByType(entry.getType()); } else { setDefaultHeaderAvatarByType(entry.getType()); } -// userAvatar.setClip(new Circle(20, 20, 20)); AvatarFX.circleClip(userAvatar, 36); - - fetchAndRenderHeader(entry); - // پیام‌ها + // --- Messages --- messageContainer.getChildren().clear(); loadMessages(entry); markAsRead(entry); - Platform.runLater(() -> messageInput.requestFocus()); - } + // --- حالت UI (Composer / Join / Add Contact) --- + applyMode(mode); + // فوکوس اگر در حالت نرمال هستیم + if (mode == ChatViewMode.NORMAL) { + Platform.runLater(() -> messageInput.requestFocus()); + } + } private void loadMessages(ChatEntry entry) { JSONObject req = new JSONObject(); @@ -967,12 +1074,17 @@ public class ChatPageController { req.put("receiver_id", entry.getId().toString()); req.put("receiver_type", entry.getType()); // باید "private" باشه - // 👇 اضافه کن: آی‌دی کاربر فعلی (current user) - UUID viewerId = UUID.fromString(Session.getUserUUID()); // هر جایی که نگه می‌داری - if ("private".equalsIgnoreCase(entry.getType()) && viewerId != null) { - req.put("viewer_id", viewerId.toString()); + String viewer = Session.getUserUUID(); // internal_uuid کاربر فعلی + if (viewer != null && !viewer.isBlank()) { + req.put("viewer_id", viewer); } +// // 👇 اضافه کن: آی‌دی کاربر فعلی (current user) +// UUID viewerId = UUID.fromString(Session.getUserUUID()); // هر جایی که نگه می‌داری +// if ("private".equalsIgnoreCase(entry.getType()) && viewerId != null) { +// req.put("viewer_id", viewerId.toString()); +// } + new Thread(() -> { JSONObject resp; try { @@ -1000,21 +1112,46 @@ public class ChatPageController { } } +// private void updatePrivateHeader(ChatEntry entry, JSONObject data) { +// String name = nz(data.optString("profile_name", entry.getName())); +// chatTitle.setText(name); +// +// // other_user_id برای ریل‌تایم status +// String other = data.optString("other_user_id", ""); +// if (!other.isBlank()) { +// try { entry.setOtherUserId(java.util.UUID.fromString(other)); } catch (Exception ignore) {} +// } +// +// // تصویر +// String img = data.optString("image_url", ""); +// if (hasVal(img)) { +// try { +// Image im = AvatarLocalResolver.load(img); // ⬅️ +// if (im != null) userAvatar.setImage(im); +// userAvatar.setClip(new Circle(20, 20, 20)); +// } catch (Exception ignore) {} +// } +// +// chatStatus.setText(userStatusText( +// data.optBoolean("online", false), +// data.optString("last_seen", null) +// )); +// +// } + private void updatePrivateHeader(ChatEntry entry, JSONObject data) { String name = nz(data.optString("profile_name", entry.getName())); chatTitle.setText(name); - // other_user_id برای ریل‌تایم status String other = data.optString("other_user_id", ""); if (!other.isBlank()) { - try { entry.setOtherUserId(java.util.UUID.fromString(other)); } catch (Exception ignore) {} + try { entry.setOtherUserId(UUID.fromString(other)); } catch (Exception ignore) {} } - // تصویر String img = data.optString("image_url", ""); if (hasVal(img)) { try { - Image im = AvatarLocalResolver.load(img); // ⬅️ + Image im = AvatarLocalResolver.load(img); if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); } catch (Exception ignore) {} @@ -1025,8 +1162,20 @@ public class ChatPageController { data.optString("last_seen", null) )); + // ⭐️ بلاک؟ + boolean blocked = data.optBoolean("blocked", false) + || data.optBoolean("is_blocked", false) + || data.optBoolean("blocked_by_me", false); + + if (blocked) { + if (readOnlyLabel != null) readOnlyLabel.setText(""); // فقط UNBLOCK را نشان بده + applyMode(ChatViewMode.BLOCKED); + } else { + applyMode(ChatViewMode.NORMAL); + } } + private void updateGroupHeader(ChatEntry entry, JSONObject data) { chatTitle.setText(nz(data.optString("group_name", entry.getName()))); @@ -1046,23 +1195,51 @@ public class ChatPageController { : (members + " members")); } +// private void updateChannelHeader(ChatEntry entry, JSONObject data) { +// chatTitle.setText(nz(data.optString("channel_name", entry.getName()))); +// +// String img = data.optString("image_url", ""); +// if (hasVal(img)) { +// try { +// Image im = AvatarLocalResolver.load(img); // ⬅️ +// if (im != null) userAvatar.setImage(im); +// userAvatar.setClip(new Circle(20, 20, 20)); +// } catch (Exception ignore) {} +// } +// +// +// int subs = data.optInt("member_count", 0); +// chatStatus.setText(subs + " subscribers"); +// } + + private void updateChannelHeader(ChatEntry entry, JSONObject data) { chatTitle.setText(nz(data.optString("channel_name", entry.getName()))); String img = data.optString("image_url", ""); if (hasVal(img)) { try { - Image im = AvatarLocalResolver.load(img); // ⬅️ + Image im = AvatarLocalResolver.load(img); if (im != null) userAvatar.setImage(im); userAvatar.setClip(new Circle(20, 20, 20)); } catch (Exception ignore) {} } - int subs = data.optInt("member_count", 0); chatStatus.setText(subs + " subscribers"); + + boolean canPost = canPostToChannel(entry, data); + if (canPost) { + applyMode(ChatViewMode.NORMAL); + Platform.runLater(() -> messageInput.requestFocus()); + } else { + if (readOnlyLabel != null) readOnlyLabel.setText("YOU CAN’T SEND MESSAGES IN THIS CHANNEL"); + applyMode(ChatViewMode.READ_ONLY); + } } + + public void onUserStatusChanged(String userUuid, String status, String lastSeenIso) { if (currentChat == null || !"private".equalsIgnoreCase(currentChat.getType())) return; var other = currentChat.getOtherUserId(); @@ -1112,4 +1289,203 @@ public class ChatPageController { java.util.Objects.requireNonNull(getClass().getResourceAsStream(path)) )); } + + @FXML + private void onJoinClicked() { + if (currentChat == null) return; + + // 1) internal_uuid کاربر فعلی (UUID) + String myInternalUuid = Session.currentUser != null + ? Session.currentUser.optString("internal_uuid", "") + : ""; + if (myInternalUuid.isBlank()) { + addSystemMessage("Join failed: missing current user internal_uuid."); + return; + } + + // 2) internal_uuid مقصد (گروه/کانال) + String targetId = currentChat.getId().toString(); + + // 3) نوع و نام اکشن + String t = currentChat.getType(); + String action = "group".equalsIgnoreCase(t) ? "join_group" : "join_channel"; + + // 4) درخواست طبق قرارداد سرور (کلیدها: user_id = UUID کاربر، id = UUID مقصد) + JSONObject req = new JSONObject() + .put("action", action) + .put("user_id", myInternalUuid) // ← UUID + .put("id", targetId); // ← UUID گروه/کانال + + JSONObject res = ActionHandler.sendWithResponse(req); + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + MainController.getInstance().onJoinedOrAdded(currentChat); + applyMode(ChatViewMode.NORMAL); + Platform.runLater(() -> messageInput.requestFocus()); + } else { + addSystemMessage("Join failed: " + (res != null ? res.optString("message","") : "no response")); + } + } + + + @FXML + private void onAddContactClicked() { + if (currentChat == null) return; + + String myUserId = Session.currentUser != null + ? Session.currentUser.optString("user_id", "") + : ""; + + // 2) internal_uuid طرف مقابل + UUID other = currentChat.getOtherUserId(); + if (other == null) { + // اگر otherUserId هنوز نگرفته‌ای، بهتره قبلش از هدر/پروفایل بیاری. + addSystemMessage("Cannot add: other user UUID is missing."); + return; + } + + // 3) درخواست طبق قرارداد سرور + JSONObject req = new JSONObject() + .put("action", "add_contact") + .put("user_id", myUserId) // ← stringِ user_id (غیر UUID) + .put("contact_id", other.toString()); // ← UUID طرف مقابل + + // 4) ارسال + JSONObject res = ActionHandler.sendWithResponse(req); + if (res != null && "success".equalsIgnoreCase(res.optString("status"))) { + // به لیست چت‌ها اضافه و سوییچ به حالت نرمال + MainController.getInstance().onJoinedOrAdded(currentChat); + applyMode(ChatViewMode.NORMAL); + Platform.runLater(() -> messageInput.requestFocus()); + } else { + addSystemMessage("Add contact failed: " + (res != null ? res.optString("message","") : "no response")); + } + } + + +// private void applyMode(ChatViewMode mode) { +// currentMode = mode; +// +// boolean normal = (mode == ChatViewMode.NORMAL); +// boolean needsJoin = (mode == ChatViewMode.NEEDS_JOIN); +// boolean needsAdd = (mode == ChatViewMode.NEEDS_ADD_CONTACT); +// +// composerPane.setVisible(normal); +// composerPane.setManaged(normal); +// +// joinPane.setVisible(needsJoin); +// joinPane.setManaged(needsJoin); +// +// addContactPane.setVisible(needsAdd); +// addContactPane.setManaged(needsAdd); +// +// if (needsJoin && joinButton != null && currentChat != null) { +// String what = "channel".equalsIgnoreCase(currentChat.getType()) ? "CHANNEL" : "GROUP"; +// joinButton.setText(("Join " + what).toUpperCase()); // => JOIN CHANNEL / JOIN GROUP +// } +// if (needsAdd && addContactButton != null) { +// addContactButton.setText("ADD CONTACT"); +// } +// } + + + + private void applyMode(ChatViewMode mode) { + currentMode = mode; + + boolean normal = (mode == ChatViewMode.NORMAL); + boolean needsJoin = (mode == ChatViewMode.NEEDS_JOIN); + boolean needsAdd = (mode == ChatViewMode.NEEDS_ADD_CONTACT); + boolean readOnly = (mode == ChatViewMode.READ_ONLY); + boolean blocked = (mode == ChatViewMode.BLOCKED); + + // Composer فقط در حالت نرمال + composerPane.setVisible(normal); + composerPane.setManaged(normal); + + // Join / Add + joinPane.setVisible(needsJoin); + joinPane.setManaged(needsJoin); + addContactPane.setVisible(needsAdd); + addContactPane.setManaged(needsAdd); + + // پنل پایین برای READ_ONLY/BLOCKED + boolean showRO = readOnly || blocked; + if (readOnlyPane != null) { + readOnlyPane.setVisible(showRO); + readOnlyPane.setManaged(showRO); + } + + // متن آبی برای READ_ONLY + if (readOnlyLabel != null) { + readOnlyLabel.setVisible(readOnly); + readOnlyLabel.setManaged(readOnly); + } + + // دکمهٔ قرمز UNBLOCK فقط در BLOCKED + if (unblockBtn != null) { + unblockBtn.setVisible(blocked); + unblockBtn.setManaged(blocked); + } + + // متن دکمه‌های Join/Add + if (needsJoin && joinButton != null && currentChat != null) { + String what = "channel".equalsIgnoreCase(currentChat.getType()) ? "CHANNEL" : "GROUP"; + joinButton.setText(("Join " + what).toUpperCase()); + } + if (needsAdd && addContactButton != null) { + addContactButton.setText("ADD CONTACT"); + } + } + + + @FXML + private void onUnblockClicked() { + if (currentChat == null) return; + UUID other = currentChat.getOtherUserId(); + if (other == null && currentChat.getDisplayId() == null) return; + + // ⚠️ با API خودت هماهنگ کن: + // این یک الگوی معمول است: user_id (کاربر فعلی) + contact_id (کسی که بلاک شده) + org.json.JSONObject req = new org.json.JSONObject() + .put("action", "unblock_user") // یا "unblock_contact" طبق سرور + .put("user_id", org.to.telegramfinalproject.Client.Session.currentUser.getString("user_id")) + .put("contact_id", (other != null) ? other.toString() : currentChat.getDisplayId()); + + org.json.JSONObject res = org.to.telegramfinalproject.Client.ActionHandler.sendWithResponse(req); + boolean ok = (res != null) && ("ok".equalsIgnoreCase(res.optString("status")) + || "success".equalsIgnoreCase(res.optString("status"))); + + if (ok) { + // برگرد به حالت نرمال: کامپوزر باز شود + applyMode(ChatViewMode.NORMAL); + Platform.runLater(() -> messageInput.requestFocus()); + } else { + // می‌تونی یک نوتیف کوچک بزنی + addSystemMessage("Unblock failed."); + } + } + + + + private boolean canPostToChannel(ChatEntry entry, JSONObject headerData) { + // 1) اگر سرور صراحتاً can_post داد، همان را بگیر + if (headerData != null && headerData.has("can_post")) { + return headerData.optBoolean("can_post", false); + } + // 2) یا اگر is_owner / is_admin را داد + if (headerData != null && (headerData.has("is_owner") || headerData.has("is_admin"))) { + return headerData.optBoolean("is_owner", false) || headerData.optBoolean("is_admin", false); + } + // 3) فال‌بک به اطلاعات لوکال: ChatEntry + permissions محلی + if (entry != null) { + if (entry.isOwner() || entry.isAdmin()) return true; + if (entry.getPermissions() != null && entry.getPermissions().optBoolean("can_post", false)) { + return true; + } + } + return false; + } + + + } diff --git a/src/main/java/org/to/telegramfinalproject/UI/ChatViewMode.java b/src/main/java/org/to/telegramfinalproject/UI/ChatViewMode.java new file mode 100644 index 0000000..459dbdd --- /dev/null +++ b/src/main/java/org/to/telegramfinalproject/UI/ChatViewMode.java @@ -0,0 +1,12 @@ +package org.to.telegramfinalproject.UI; + + +//For search handling +public enum ChatViewMode { + NORMAL, // member/contact; can send messages + NEEDS_JOIN, // group/channel preview; show Join button + NEEDS_ADD_CONTACT, // private preview; show Add Contact button + READ_ONLY, + BLOCKED + } + diff --git a/src/main/java/org/to/telegramfinalproject/UI/MainController.java b/src/main/java/org/to/telegramfinalproject/UI/MainController.java index b1ccaf8..16bffad 100644 --- a/src/main/java/org/to/telegramfinalproject/UI/MainController.java +++ b/src/main/java/org/to/telegramfinalproject/UI/MainController.java @@ -40,6 +40,7 @@ public class MainController { GLOBAL, CHAT } + private ChatViewMode currentMode = ChatViewMode.NORMAL; // حالت فعلی: NORMAL/NEEDS_JOIN/NEEDS_ADD_CONTACT private SearchMode currentSearchMode = SearchMode.GLOBAL; private UUID currentChatId; // if in CHAT mode, which chat to search in @@ -423,6 +424,9 @@ public class MainController { FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); Node chatPage = loader.load(); + + + ChatPageController controller = loader.getController(); controller.showChat(chat); @@ -439,6 +443,30 @@ public class MainController { } } + + private void openChatWithMode(ChatEntry chat, ChatViewMode mode) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml")); + Node chatPage = loader.load(); + + ChatPageController controller = loader.getController(); + controller.showChat(chat, mode); // ⬅️ متد جدید در ChatPageController + + this.chatPageController = controller; + Session.currentChatId = chat.getId().toString(); + + chatDisplayArea.getChildren().setAll(chatPage); + + chat.setUnreadCount(0); + ChatItemController item = itemControllers.get(chat.getId()); + if (item != null) item.setUnread(0); + + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + @FXML private void toggleSidebar() { if (isSidebarOpen) { @@ -832,39 +860,52 @@ public class MainController { private void openSearchResult(SearchResult r) { switch (r.type) { case USER: { + // 1) اگه قبلاً چت پرایوت با این یوزر داری، از همون استفاده کن + UUID existingChatId = findExistingPrivateChatId(r.uuid); - java.util.UUID chatId = findExistingPrivateChatId(r.uuid); - if (chatId == null) { - chatId = fetchOrCreatePrivateChat(r.uuid); - if (chatId == null) { - System.out.println("❌ Failed to create/find private chat."); - return; - } + ChatEntry ce = new ChatEntry(); + ce.setType("private"); + ce.setName(r.title); + ce.setDisplayId(r.displayId); + try { ce.setOtherUserId(r.uuid); } catch (Exception ignore) {} + + ChatViewMode mode; + if (existingChatId != null) { + ce.setId(existingChatId.toString()); + mode = ChatViewMode.NORMAL; + } else { + // هنوز چتی وجود ندارد → Preview (بدون ساخت چت) + // برای Preview از uuid خودِ طرف مقابل به‌عنوان id موقت استفاده می‌کنیم + ce.setId(r.uuid.toString()); + mode = isContact(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_ADD_CONTACT; } - org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); - ce.setId(chatId.toString()); // ⬅️ internal chat_id - ce.setDisplayId(r.displayId); // username - ce.setName(r.title); // profile_name - ce.setType("private"); - - openChat(ce); + openChatWithMode(ce, mode); break; } + case GROUP: case CHANNEL: { org.to.telegramfinalproject.Models.ChatEntry existing = findExistingChat(r.uuid, r.receiverType); if (existing != null) { - openChat(existing); + openChatWithMode(existing, ChatViewMode.NORMAL); } else { org.to.telegramfinalproject.Models.ChatEntry ce = new org.to.telegramfinalproject.Models.ChatEntry(); ce.setId(r.uuid.toString()); // internal_uuid group/channel ce.setDisplayId(r.displayId); // group_id/channel_id ce.setName(r.title); ce.setType(r.receiverType); - openChat(ce); + //openChat(ce); + ChatViewMode mode; + try { + mode = isInAnyChatList(r.uuid) ? ChatViewMode.NORMAL : ChatViewMode.NEEDS_JOIN; + } catch (Exception e) { + mode = ChatViewMode.NEEDS_JOIN; + } + openChatWithMode(ce, mode); + } break; } @@ -968,4 +1009,87 @@ public class MainController { return false; } + + + //Search + private boolean isInAnyChatList(UUID chatId) { + var lists = List.of( + Session.chatList != null ? Session.chatList : List.of(), + Session.activeChats != null ? Session.activeChats : List.of(), + Session.archivedChats != null ? Session.archivedChats : List.of() + ); + for (var lst : lists) { + for (var c : lst) { + try { + if (chatId.equals(UUID.fromString(c.getId().toString()))) return true; + } catch (Exception ignore) {} + } + } + return false; + } + + + private boolean isContact(UUID userUuid) { + if (Session.contactEntries == null) return false; + try { + for (var c : Session.contactEntries) { + UUID id = c.getContactId(); + if (userUuid.equals(id)) return true; + } + } catch (Exception ignore) {} + return false; + } + + + + private ChatViewMode computeMode(ChatEntry ce) { + try { + UUID id = UUID.fromString(ce.getId().toString()); + if (isInAnyChatList(id)) return ChatViewMode.NORMAL; + } catch (Exception ignore) {} + + String t = ce.getType(); + if ("group".equalsIgnoreCase(t) || "channel".equalsIgnoreCase(t)) { + return ChatViewMode.NEEDS_JOIN; + } + if ("private".equalsIgnoreCase(t)) { + UUID other = null; + try { other = ce.getOtherUserId(); } catch (Exception ignore) {} + return (other != null && isContact(other)) + ? ChatViewMode.NORMAL + : ChatViewMode.NEEDS_ADD_CONTACT; + } + return ChatViewMode.NORMAL; + } + + public void onJoinedOrAdded(ChatEntry ce) { + try { + UUID id = UUID.fromString(ce.getId().toString()); + if (!isInAnyChatList(id)) { + if (Session.chatList == null) Session.chatList = new ArrayList<>(); + Session.chatList.add(ce); + } + // اگر activeChats استفاده می‌کنی: + if (Session.activeChats != null && Session.activeChats.stream().noneMatch(c -> id.equals(c.getId()))) { + Session.activeChats.add(ce); + } + } catch (Exception ignore) {} + + // (اختیاری) مرتب‌سازی بر اساس زمان آخرین پیام + Comparator byTimeDesc = (a,b) -> { + LocalDateTime t1 = a.getLastMessageTime(), t2 = b.getLastMessageTime(); + if (t1 == null && t2 == null) return 0; + if (t1 == null) return 1; + if (t2 == null) return -1; + return t2.compareTo(t1); + }; + if (Session.chatList != null) Session.chatList.sort(byTimeDesc); + if (Session.activeChats != null) Session.activeChats.sort(byTimeDesc); + + refreshChatListUI(); + } + + + + } \ No newline at end of file diff --git a/src/main/resources/org/to/telegramfinalproject/CSS/chat.css b/src/main/resources/org/to/telegramfinalproject/CSS/chat.css new file mode 100644 index 0000000..5def59d --- /dev/null +++ b/src/main/resources/org/to/telegramfinalproject/CSS/chat.css @@ -0,0 +1,76 @@ +/* دقیقاً فقط روی این دو دکمه اعمال می‌شود */ +#joinAction, #addContactAction { + -fx-background-color: transparent, transparent, transparent, transparent; + -fx-background-insets: 0,0,0,0; + -fx-background-radius: 0,0,0,0; + -fx-border-color: transparent; + -fx-border-width: 0; + -fx-text-fill: #1e88e5; /* آبی لینک */ + -fx-font-weight: 700; + -fx-font-size: 12px; + -fx-padding: 12 0 12 0; + -fx-cursor: hand; + -fx-effect: null; +} + +#joinAction:hover, #addContactAction:hover { + -fx-underline: true; +} + +#joinAction:focused, #addContactAction:focused { + -fx-underline: true; + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} + +#joinAction:armed, #addContactAction:armed { + -fx-opacity: .85; +} + +/* ظرف بنر پایین */ +.chat-footer-banner { + -fx-background-color: transparent; +} + +/* دکمهٔ لینک‌مانند (مشترک) */ +.footer-link-btn { + -fx-background-color: transparent; + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-padding: 6 0 6 0; /* نازک مثل لینک */ + -fx-border-color: transparent; + -fx-font-size: 14px; + -fx-font-weight: 700; + -fx-text-fill: #1a73e8; /* پیش‌فرض آبی (برای Join/Add) */ +} + +/* کانتینر بنر پایین چت */ +.chat-footer-banner { + -fx-background-color: transparent; + -fx-alignment: center; +} + +/* متن آبی برای حالت Read-only */ +.chat-footer-banner .banner-text-blue { + -fx-text-fill: #1E88E5; /* آبی */ + -fx-font-weight: 700; + -fx-background-color: transparent; +} + +/* لینک‌استایل دکمه‌ها در بنر */ +.chat-footer-banner .footer-link-btn { + -fx-background-color: transparent; + -fx-text-fill: #1E88E5; /* آبی پیش‌فرض */ + -fx-font-weight: 700; + -fx-padding: 6 12; + -fx-background-insets: 0; + -fx-cursor: hand; +} +.chat-footer-banner .footer-link-btn:hover { + -fx-underline: true; +} + +/* نسخه قرمز برای UNBLOCK */ +.chat-footer-banner .footer-link-btn.danger { + -fx-text-fill: #D32F2F; /* قرمز */ +} diff --git a/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml b/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml index e577564..2660490 100644 --- a/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml +++ b/src/main/resources/org/to/telegramfinalproject/Fxml/chat_page.fxml @@ -6,17 +6,19 @@ + + + + - - - + @@ -33,9 +35,7 @@ @@ -44,9 +44,7 @@ + + +