Merge branch 'develop' into Main-UI
# Conflicts: # src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java # src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java # src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java # src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java # src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java
This commit is contained in:
@@ -12,6 +12,10 @@ 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.*;
|
||||
|
||||
@@ -20,6 +24,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;
|
||||
@@ -30,11 +43,60 @@ public class ClientHandler implements Runnable {
|
||||
UUID userId = null;
|
||||
|
||||
try (
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
|
||||
|
||||
// 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);
|
||||
|
||||
) {
|
||||
|
||||
// DataInputStream bin = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
|
||||
|
||||
String inputLine;
|
||||
while ((inputLine = in.readLine()) != null) {
|
||||
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;
|
||||
}
|
||||
|
||||
JSONObject requestJson = new JSONObject(inputLine);
|
||||
String action = requestJson.getString("action");
|
||||
ResponseModel response = null;
|
||||
@@ -2275,6 +2337,13 @@ public class ClientHandler implements Runnable {
|
||||
|
||||
List<Message> messages = MessageDatabase.getMessagesForChat(chatId, chatType, currentUser.getInternal_uuid(), offset, limit);
|
||||
|
||||
java.util.List<UUID> mids = new java.util.ArrayList<>();
|
||||
for (Message m : messages) mids.add(m.getMessage_id());
|
||||
|
||||
// ⬅️ همهٔ اتچمنتها را یکجا بگیر: message_id -> list(attachments)
|
||||
java.util.Map<UUID, java.util.List<MediaRow>> attMap =
|
||||
MessageDatabase.findAttachmentsForMessages(mids);
|
||||
|
||||
JSONArray result = new JSONArray();
|
||||
for (Message m : messages) {
|
||||
JSONObject obj = new JSONObject();
|
||||
@@ -2342,6 +2411,26 @@ public class ClientHandler implements Runnable {
|
||||
obj.put("reactions", new JSONArray(reactions));
|
||||
|
||||
|
||||
JSONArray atts = new JSONArray();
|
||||
java.util.List<MediaRow> 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);
|
||||
}
|
||||
|
||||
@@ -2870,7 +2959,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
|
||||
@@ -2895,9 +2986,606 @@ 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<FileAttachment> 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<UUID> 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<FileAttachment> 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<UUID> 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.");
|
||||
@@ -2905,57 +3593,71 @@ 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"));
|
||||
UUID 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.");
|
||||
|
||||
// Parse attachments
|
||||
List<FileAttachment> attachments = new ArrayList<>();
|
||||
if (json.has("attachments")) {
|
||||
JSONArray attachmentsArray = json.getJSONArray("attachments");
|
||||
List<FileAttachment> 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
|
||||
Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now());
|
||||
List<UUID> receivers = getReceiversForChat(receiverId, receiverType);
|
||||
receivers.remove(senderId);
|
||||
RealTimeEventDispatcher.sendNewMessage(msg, receivers);
|
||||
if ((content == null || content.isBlank()) && attachments.isEmpty()) {
|
||||
return new ResponseModel("error", "Empty message: no content or attachment.");
|
||||
}
|
||||
|
||||
// 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());
|
||||
// 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 chatPayload = new JSONObject();
|
||||
chatPayload.put("action", "chat_updated");
|
||||
@@ -2967,9 +3669,76 @@ public class ClientHandler implements Runnable {
|
||||
|
||||
RealTimeEventDispatcher.sendToUser(senderId, chatPayload);
|
||||
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("action", "new_message");
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("message_id", messageId.toString());
|
||||
return new ResponseModel("success", "Message sent successfully.", data);
|
||||
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<UUID> 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<UUID> 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<UUID> 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);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@@ -2978,6 +3747,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private List<UUID> getReceiversForChat(UUID receiverId, String receiverType) {
|
||||
switch (receiverType) {
|
||||
case "private":
|
||||
|
||||
@@ -7,7 +7,8 @@ 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) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
|
||||
|
||||
@@ -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<UUID> 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; }
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package org.to.telegramfinalproject.Server;
|
||||
|
||||
import static spark.Spark.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import javax.sound.sampled.*; // برای WAV
|
||||
|
||||
import org.json.JSONObject;
|
||||
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/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);
|
||||
|
||||
//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 ("AUDIO".equals(fileType)) {
|
||||
durationSeconds = audioDurationSeconds(target, mime, ext);
|
||||
}
|
||||
|
||||
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", JSONObject.NULL)
|
||||
.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("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("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 ("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", "");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//only audio
|
||||
private static Integer audioDurationSeconds(Path file, String mime, String ext) {
|
||||
try {
|
||||
if ("audio/mpeg".equalsIgnoreCase(mime) || ".mp3".equalsIgnoreCase(ext)) {
|
||||
Mp3File mp3 = new Mp3File(file.toFile());
|
||||
return (int) mp3.getLengthInSeconds();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user