Work on sending files

This commit is contained in:
2025-08-13 21:19:50 +03:30
parent eb633b5e13
commit 5689cb4267
4 changed files with 295 additions and 78 deletions
@@ -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;
@@ -84,25 +84,54 @@ public class MessageDatabase {
public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List<FileAttachment> 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<FileAttachment> attachments) {
public static boolean saveMessageWithOptionalAttachments(
UUID messageId, UUID senderId, UUID receiverId,
String receiverType, String content, String messageType,
List<FileAttachment> 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();
@@ -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;
}
}
@@ -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<FileAttachment> 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<UUID> others = new ArrayList<>(allMembers);
others.remove(senderId);
RealTimeEventDispatcher.broadcastToUsers(others, payload);