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); JSONObject m = messages.getJSONObject(i);
String senderId = m.getString("sender_id"); String senderId = m.getString("sender_id");
String senderName = m.optString("sender_name", "Other"); String senderName = m.optString("sender_name", "Other");
String content = m.getString("content"); String content = m.optString("content", "");
String time = m.getString("send_at"); String time = m.getString("send_at");
String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName; 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 { public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List<FileAttachment> attachments) throws SQLException {
if (attachments == null || attachments.isEmpty()) return true; if (attachments == null || attachments.isEmpty()) return true;
String sql = """
INSERT INTO message_attachments final String sql = """
(attachment_id, message_id, file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url) INSERT INTO message_attachments(
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 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)) { try (PreparedStatement ps = conn.prepareStatement(sql)) {
for (FileAttachment att : attachments) { for (FileAttachment att : attachments) {
ps.setObject(1, UUID.randomUUID()); if (att == null) throw new IllegalArgumentException("Attachment is null");
ps.setObject(2, messageId); UUID attachmentId = att.getAttachmentId() != null ? att.getAttachmentId() : UUID.randomUUID();
ps.setString(3, att.getFileUrl()); UUID mediaKey = att.getMediaKey() != null ? att.getMediaKey() : attachmentId; // ساده‌ترین حالت
ps.setString(4, att.getFileType()); // IMAGE/VIDEO/AUDIO/FILE/GIF/STICKER
ps.setString(5, att.getFileName()); String ft = att.getFileType();
if (att.getFileSize() != null) ps.setLong(6, att.getFileSize()); else ps.setNull(6, java.sql.Types.BIGINT); if (!"IMAGE".equalsIgnoreCase(ft) && !"AUDIO".equalsIgnoreCase(ft)) {
ps.setString(7, att.getMimeType()); throw new IllegalArgumentException("file_type must be IMAGE or AUDIO");
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.getStoragePath() == null || att.getStoragePath().isBlank()) {
if (att.getDurationSeconds() != null) ps.setInt(10, att.getDurationSeconds()); else ps.setNull(10, java.sql.Types.INTEGER); throw new IllegalArgumentException("storage_path is required for socket downloads");
ps.setString(11, att.getThumbnailUrl()); }
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(); ps.addBatch();
att.setAttachmentId(attachmentId);
att.setMediaKey(mediaKey);
} }
ps.executeBatch(); ps.executeBatch();
return true; 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, public static boolean saveMessageWithOptionalAttachments(
List<FileAttachment> attachments) { UUID messageId, UUID senderId, UUID receiverId,
String receiverType, String content, String messageType,
List<FileAttachment> attachments
) {
Connection conn = null; Connection conn = null;
try { try {
conn = ConnectionDb.connect(); conn = ConnectionDb.connect();
conn.setAutoCommit(false); 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 (isText) {
if (attachments != null && !attachments.isEmpty()) if (attachments != null && !attachments.isEmpty())
throw new IllegalArgumentException("TEXT must not have attachments"); throw new IllegalArgumentException("TEXT must not have attachments");
@@ -127,9 +165,18 @@ public class MessageDatabase {
} else { } else {
if (attachments == null || attachments.isEmpty()) if (attachments == null || attachments.isEmpty())
throw new IllegalArgumentException("Non-TEXT must have at least one attachment"); 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); if (!isText) insertAttachmentsTx(conn, messageId, attachments);
conn.commit(); conn.commit();
@@ -2,18 +2,22 @@ package org.to.telegramfinalproject.Models;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
public class FileAttachment { public class FileAttachment {
private final String fileUrl; private UUID attachmentId; // اختیاری؛ اگر null بود، تولید می‌کنیم
private final String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER private UUID mediaKey;
private final String fileName; private String fileUrl;
private final Long fileSize; private String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER
private final String mimeType; // e.g., image/png private String fileName;
private final Integer width; private Long fileSize;
private final Integer height; private String mimeType; // e.g., image/png
private final Integer durationSeconds; // for audio/video private Integer width;
private final String thumbnailUrl; private Integer height;
private Integer durationSeconds; // for audio/video
private String thumbnailUrl;
private String storagePath;
public FileAttachment(String fileUrl, public FileAttachment(String fileUrl,
String fileType, String fileType,
@@ -39,6 +43,10 @@ public class FileAttachment {
this(fileUrl, fileType, null, null, null, null, null, null, null); this(fileUrl, fileType, null, null, null, null, null, null, null);
} }
public FileAttachment() {
}
// ساخت از JSON /upload // ساخت از JSON /upload
public static FileAttachment fromUploadJson(JSONObject j) { public static FileAttachment fromUploadJson(JSONObject j) {
return new FileAttachment( return new FileAttachment(
@@ -54,7 +62,6 @@ public class FileAttachment {
); );
} }
// خروجی JSON برای RT/کلاینت
public JSONObject toJson() { public JSONObject toJson() {
JSONObject out = new JSONObject() JSONObject out = new JSONObject()
.put("file_url", fileUrl) .put("file_url", fileUrl)
@@ -123,4 +130,33 @@ public class FileAttachment {
", thumbnailUrl='" + thumbnailUrl + '\'' + ", 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.io.*;
import java.net.Socket; 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.time.LocalDateTime;
import java.util.*; 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) { private void handleMediaFrame(DataInputStream dis, PrintWriter out) {
try { try {
// MAGIC = "MDM1" final int MAGIC_EXPECTED = 0x4D444D31; // "MDM1"
final int MAGIC_EXPECTED = 0x4D444D31;
int magic = dis.readInt(); int magic = dis.readInt();
if (magic != MAGIC_EXPECTED) { if (magic != MAGIC_EXPECTED) {
out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); out.flush(); return;
out.flush();
return;
} }
int headerLen = dis.readInt(); int headerLen = dis.readInt();
if (headerLen <= 0 || headerLen > (64 * 1024)) { if (headerLen <= 0 || headerLen > 64 * 1024) {
out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); out.flush(); return;
out.flush();
return;
} }
byte[] headerBytes = dis.readNBytes(headerLen); byte[] headerBytes = dis.readNBytes(headerLen);
if (headerBytes.length != headerLen) { if (headerBytes.length != headerLen) {
out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); out.flush(); return;
out.flush();
return;
} }
JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8)); JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8));
long contentLen = dis.readLong(); long contentLen = dis.readLong();
long MAX_MEDIA = 25L * 1024 * 1024; long MAX_MEDIA = 25L * 1024 * 1024;
if (contentLen <= 0 || contentLen > MAX_MEDIA) { if (contentLen <= 0 || contentLen > MAX_MEDIA) {
skip(dis, contentLen); skip(dis, contentLen);
out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); out.flush(); return;
out.flush();
return;
} }
// الزامی‌ها UUID messageId = UUID.fromString(h.getString("message_id"));
UUID messageId = UUID.fromString(h.getString("message_id")); UUID senderId = UUID.fromString(h.getString("sender_id"));
UUID senderId = UUID.fromString(h.getString("sender_id")); String rType = h.getString("receiver_type"); // private/group/channel
String rType = h.getString("receiver_type"); // private/group/channel UUID receiverId = UUID.fromString(h.getString("receiver_id"));
UUID receiverId = UUID.fromString(h.getString("receiver_id")); String messageType = h.getString("message_type").toUpperCase(); // IMAGE | AUDIO
String messageType = h.getString("message_type"); // IMAGE | AUDIO
if (!"IMAGE".equalsIgnoreCase(messageType) && !"AUDIO".equalsIgnoreCase(messageType)) { if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType)) {
skip(dis, contentLen); skip(dis, contentLen);
out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); out.flush(); return;
out.flush();
return;
} }
String fileName = h.optString("file_name", "file.bin"); String fileName = h.optString("file_name", "file.bin");
String mimeType = h.optString("mime_type", "application/octet-stream"); 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 width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null;
Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null; Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null;
if (fileName.length() > 200) fileName = fileName.substring(0, 200); 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.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize();
java.nio.file.Files.createDirectories(baseDir); 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(); String subdir = kind + "/" + java.time.LocalDate.now();
java.nio.file.Path dir = baseDir.resolve(subdir).normalize(); java.nio.file.Path dir = baseDir.resolve(subdir).normalize();
java.nio.file.Files.createDirectories(dir); java.nio.file.Files.createDirectories(dir);
@@ -2679,7 +2791,7 @@ public class ClientHandler implements Runnable {
String storedName = java.util.UUID.randomUUID() + ext; String storedName = java.util.UUID.randomUUID() + ext;
java.nio.file.Path target = dir.resolve(storedName).normalize(); java.nio.file.Path target = dir.resolve(storedName).normalize();
// دریافت بایت‌های فایل // دریافت باینری فایل
try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream( try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream(
target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) { target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) {
long remaining = contentLen; long remaining = contentLen;
@@ -2694,30 +2806,52 @@ public class ClientHandler implements Runnable {
} }
long fileSize = java.nio.file.Files.size(target); long fileSize = java.nio.file.Files.size(target);
String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName;
FileAttachment att = new FileAttachment( String storagePath = target.toString(); // فقط سرور استفاده کنه
fileUrl, String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName; // اختیاری/نمایشی (HTTP لازم نیست)
messageType.toUpperCase(), // IMAGE/AUDIO String mt = messageType; // "IMAGE" یا "AUDIO"
fileName, int safeWidth = ("IMAGE".equals(mt) && width != null) ? width : 0;
fileSize, int safeHeight = ("IMAGE".equals(mt) && height != null) ? height : 0;
mimeType, FileAttachment att = new FileAttachment();
width, att.setFileUrl(fileUrl); // اختیاری
height, att.setFileType(messageType); // IMAGE/AUDIO
null, // durationSeconds att.setFileName(fileName);
null // thumbnailUrl 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( 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() JSONObject ack = new JSONObject()
.put("status", ok ? "success" : "error") .put("status", ok ? "success" : "error")
.put("message_id", messageId.toString()) .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("file_size", fileSize)
.put("mime_type", mimeType); .put("mime_type", mimeType)
.put("display_path", fileUrl); // صرفاً نمایشی
out.println(ack.toString()); out.println(ack.toString());
out.flush(); out.flush();
@@ -2729,6 +2863,7 @@ public class ClientHandler implements Runnable {
} }
} }
private static void skip(DataInputStream dis, long n) throws IOException { private static void skip(DataInputStream dis, long n) throws IOException {
if (n <= 0) return; if (n <= 0) return;
byte[] buf = new byte[8192]; byte[] buf = new byte[8192];
@@ -2997,7 +3132,6 @@ public class ClientHandler implements Runnable {
.put("data", chatUpdate); .put("data", chatUpdate);
for (UUID u : allMembers) RealTimeEventDispatcher.sendToUser(u, chatPayload); for (UUID u : allMembers) RealTimeEventDispatcher.sendToUser(u, chatPayload);
// حالا new_message را فقط به غیر از sender
List<UUID> others = new ArrayList<>(allMembers); List<UUID> others = new ArrayList<>(allMembers);
others.remove(senderId); others.remove(senderId);
RealTimeEventDispatcher.broadcastToUsers(others, payload); RealTimeEventDispatcher.broadcastToUsers(others, payload);