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,
public static boolean saveMessageWithOptionalAttachments(
UUID messageId, UUID senderId, UUID receiverId,
String receiverType, String content, String messageType, String receiverType, String content, String messageType,
List<FileAttachment> attachments) { 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"); // IMAGE | AUDIO 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); 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);