From 17366f5ea64fd67daf0dfffb3d8437c8a2bfa5a0 Mon Sep 17 00:00:00 2001 From: Partow Roshani Date: Sun, 10 Aug 2025 12:46:05 +0330 Subject: [PATCH] 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; + } + } +}