This commit is contained in:
2025-09-02 18:12:49 +03:30
parent 0ab8f38011
commit e62ffbc655
6 changed files with 271 additions and 1829 deletions
@@ -2,18 +2,22 @@ package org.to.telegramfinalproject.Client;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.to.telegramfinalproject.Database.PrivateChatDatabase;
import org.to.telegramfinalproject.Database.ContactDatabase;
import org.to.telegramfinalproject.Models.ChatEntry; import org.to.telegramfinalproject.Models.ChatEntry;
import org.to.telegramfinalproject.Models.ContactEntry; import org.to.telegramfinalproject.Models.ContactEntry;
import org.to.telegramfinalproject.Models.SearchRequestModel; import org.to.telegramfinalproject.Models.SearchRequestModel;
import org.to.telegramfinalproject.Models.SearchResultModel;
import java.io.*; import java.io.BufferedReader;
import java.nio.file.Files; import java.io.IOException;
import java.nio.file.Path; import java.io.PrintWriter;
import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class ActionHandler { public class ActionHandler {
@@ -22,7 +26,6 @@ public class ActionHandler {
private final Scanner scanner; private final Scanner scanner;
public static volatile boolean forceExitChat = false; public static volatile boolean forceExitChat = false;
public static ActionHandler instance; public static ActionHandler instance;
private final DataOutputStream outBin;
//use for UI //use for UI
private volatile String lastStatus = "error"; // success | error private volatile String lastStatus = "error"; // success | error
@@ -54,12 +57,12 @@ public class ActionHandler {
} }
} }
public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) { public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) {
this.out = out; this.out = out;
this.in = in; this.in = in;
this.outBin = outBin;
this.scanner = scanner; this.scanner = scanner;
ActionHandler.instance = this; ActionHandler.instance = this;
} }
public void login(String username , String password){ public void login(String username , String password){
@@ -542,12 +545,6 @@ public class ActionHandler {
case "register": case "register":
Session.currentUser = response.getJSONObject("data"); Session.currentUser = response.getJSONObject("data");
//for downloaded medias
UUID accountId = UUID.fromString(Session.currentUser.getString("internal_uuid"));
Session.downloadsIndex = new DownloadsIndex(accountId);
JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list"); JSONArray chatListJson = Session.currentUser.getJSONArray("chat_list");
JSONArray Archived = Session.currentUser.getJSONArray("archived_chat_list"); JSONArray Archived = Session.currentUser.getJSONArray("archived_chat_list");
JSONArray Active = Session.currentUser.getJSONArray("active_chat_list"); JSONArray Active = Session.currentUser.getJSONArray("active_chat_list");
@@ -662,6 +659,7 @@ public class ActionHandler {
Session.activeChats = activeChats; Session.activeChats = activeChats;
Session.archivedChats = archivedChats; Session.archivedChats = archivedChats;
Session.chatList = chatList; Session.chatList = chatList;
@@ -1471,7 +1469,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.optString("content", ""); String content = m.getString("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;
@@ -1693,7 +1691,7 @@ public class ActionHandler {
String input = scanner.nextLine().trim(); String input = scanner.nextLine().trim();
switch (input) { switch (input) {
case "1" -> sendMessageInteractive(chatId, "private"); case "1" -> sendMessage(chatId, "private");
case "2" -> { viewMessagesInChat(chat); } case "2" -> { viewMessagesInChat(chat); }
case "3" -> { return false; } case "3" -> { return false; }
default -> System.out.println("Invalid choice."); default -> System.out.println("Invalid choice.");
@@ -1712,7 +1710,7 @@ public class ActionHandler {
String input = scanner.nextLine().trim(); String input = scanner.nextLine().trim();
switch (input) { switch (input) {
case "1" -> sendMessageInteractive(chat.getId(), "private"); case "1" -> sendMessage(chatId, "private");
case "2" -> toggleBlock(chat.getOtherUserId()); case "2" -> toggleBlock(chat.getOtherUserId());
case "3" -> { deleteChat(chatId, false); return true; } case "3" -> { deleteChat(chatId, false); return true; }
case "4" -> { deleteChat(chatId, true); return true; } case "4" -> { deleteChat(chatId, true); return true; }
@@ -1808,7 +1806,7 @@ public class ActionHandler {
String input = scanner.nextLine(); String input = scanner.nextLine();
switch (input) { switch (input) {
case "1" -> sendMessageInteractive(chat.getId(), "group"); case "1" -> sendMessage(chat.getId(), "group");
case "2" -> viewGroupMembers(chat.getId()); case "2" -> viewGroupMembers(chat.getId());
case "3" -> { case "3" -> {
if (isOwner || (isAdmin && perms.optBoolean("can_add_members", false))) if (isOwner || (isAdmin && perms.optBoolean("can_add_members", false)))
@@ -1935,7 +1933,7 @@ public class ActionHandler {
switch (input) { switch (input) {
case "1" -> { case "1" -> {
if (isOwner || (isAdmin && perms.optBoolean("can_post", false))) { if (isOwner || (isAdmin && perms.optBoolean("can_post", false))) {
sendMessageInteractive(chat.getId(), "channel"); sendMessage(chat.getId(), "channel");
} else { } else {
System.out.println("❌ You don't have permission to post."); System.out.println("❌ You don't have permission to post.");
} }
@@ -3675,135 +3673,82 @@ public class ActionHandler {
// public void sendMessage(UUID chatId, String receiverType) { public void sendMessage(UUID chatId, String receiverType) {
// Scanner scanner = new Scanner(System.in); Scanner scanner = new Scanner(System.in);
//
// System.out.print("Enter your message: ");
// String content = scanner.nextLine();
//
// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE): ");
// String messageType = scanner.nextLine().toUpperCase();
// Set<String> allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE");
// while (!allowedTypes.contains(messageType)) {
// System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE): ");
// messageType = scanner.nextLine().toUpperCase();
// }
//
// JSONArray attachmentsArray = new JSONArray();
// System.out.print("Do you want to attach files? (yes/no): ");
// if (scanner.nextLine().equalsIgnoreCase("yes")) {
// while (true) {
// System.out.print("File URL: ");
// String fileUrl = scanner.nextLine();
// System.out.print("File Type (IMAGE / VIDEO / FILE): ");
// String fileType = scanner.nextLine().toUpperCase();
//
// JSONObject fileJson = new JSONObject();
// fileJson.put("file_url", fileUrl);
// fileJson.put("file_type", fileType);
// attachmentsArray.put(fileJson);
//
// System.out.print("Add another file? (yes/no): ");
// if (!scanner.nextLine().equalsIgnoreCase("yes")) break;
// }
// }
//
//
//
// JSONObject messageJson = new JSONObject();
// messageJson.put("action", "send_message");
// messageJson.put("receiver_type", receiverType);
// messageJson.put("receiver_id", chatId.toString());
// messageJson.put("content", content);
// messageJson.put("message_type", messageType);
//
// if (!attachmentsArray.isEmpty()) {
// messageJson.put("attachments", attachmentsArray);
// }
//
// JSONObject response = sendWithResponse(messageJson);
// if (response != null && response.getString("status").equals("success")) {
// System.out.println("✅ Message sent! ID: " + response.getJSONObject("data").getString("message_id"));
// } else {
// System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "No response"));
// }
// }
System.out.print("Enter your message: ");
String content = scanner.nextLine();
// public void sendMessage(UUID chatId, String receiverType) { System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE / AUDIO): ");
// Scanner scanner = new Scanner(System.in); String messageType = scanner.nextLine().toUpperCase();
// Set<String> allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE");
// System.out.print("Enter your message (leave empty if file only): "); while (!allowedTypes.contains(messageType)) {
// String content = scanner.nextLine(); System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE / AUDIO): ");
// messageType = scanner.nextLine().toUpperCase();
// System.out.print("Enter message type (TEXT / IMAGE / AUDIO / FILE / GIF): "); }
// String messageType = scanner.nextLine().toUpperCase();
// Set<String> allowedTypes = Set.of("TEXT", "IMAGE", "AUDIO", "FILE", "GIF"); JSONArray attachmentsArray = new JSONArray();
// while (!allowedTypes.contains(messageType)) { System.out.print("Do you want to attach files? (yes/no): ");
// System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / AUDIO / FILE / GIF): "); if (scanner.nextLine().equalsIgnoreCase("yes")) {
// messageType = scanner.nextLine().toUpperCase(); while (true) {
// } System.out.print("File URL: ");
// String fileUrl = scanner.nextLine();
// JSONArray attachmentsArray = new JSONArray();
// System.out.print("Attach files? (yes/no): "); // URL validation
// if (scanner.nextLine().equalsIgnoreCase("yes")) { if (fileUrl.isEmpty()) {
// while (true) { System.out.print("URL can not be empty. Try again.");
// System.out.println("Paste the JSON you got from /upload (or leave empty to enter minimal fields):"); continue;
// String jsonLine = scanner.nextLine().trim(); }
//
// JSONObject fileJson; if (fileUrl.contains(" ")) {
// if (!jsonLine.isEmpty()) { System.out.println("URL cannot contain spaces. Try again.");
// // انتظار خروجی کامل /upload continue;
// fileJson = new JSONObject(jsonLine); }
// // اگه خروجی /upload تو ریشه‌ست، تبدیلش کن به ساختار attachment
// fileJson = new JSONObject() if (!fileUrl.isEmpty() && !fileUrl.matches("^(http|https)://.*$")) {
// .put("file_url", fileJson.optString("file_url", "")) System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link.");
// .put("file_type", fileJson.optString("file_type", "FILE")) continue;
// .put("file_name", fileJson.optString("file_name", "")) }
// .put("file_size", fileJson.optLong("file_size", 0))
// .put("mime_type", fileJson.optString("mime_type", "")) System.out.print("File Type (IMAGE / VIDEO / FILE / AUDIO): ");
// .put("width", fileJson.isNull("width") ? JSONObject.NULL : fileJson.optInt("width")) String fileType = scanner.nextLine().toUpperCase();
// .put("height", fileJson.isNull("height") ? JSONObject.NULL : fileJson.optInt("height"))
// .put("duration_seconds", fileJson.isNull("duration_seconds") ? JSONObject.NULL : fileJson.optInt("duration_seconds")) Set<String> allowedFileTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE");
// .put("thumbnail_url", fileJson.isNull("thumbnail_url") ? JSONObject.NULL : fileJson.optString("thumbnail_url", null)); while (!allowedFileTypes.contains(fileType)) {
// } else { System.out.print("❌ Invalid type. Try again (IMAGE / VIDEO / FILE / AUDIO): ");
// // ورودی حداقلی fileType = scanner.nextLine().toUpperCase();
// System.out.print("File URL: "); }
// String fileUrl = scanner.nextLine();
// System.out.print("File Type (IMAGE / AUDIO / FILE / GIF): "); JSONObject fileJson = new JSONObject();
// String fileType = scanner.nextLine().toUpperCase(); fileJson.put("file_url", fileUrl);
// fileJson.put("file_type", fileType);
// fileJson = new JSONObject(); attachmentsArray.put(fileJson);
// fileJson.put("file_url", fileUrl);
// fileJson.put("file_type", fileType); System.out.print("Add another file? (yes/no): ");
// } if (!scanner.nextLine().equalsIgnoreCase("yes")) break;
// }
// attachmentsArray.put(fileJson); }
//
// System.out.print("Add another file? (yes/no): "); JSONObject messageJson = new JSONObject();
// if (!scanner.nextLine().equalsIgnoreCase("yes")) break; messageJson.put("action", "send_message");
// } messageJson.put("receiver_type", receiverType);
// } messageJson.put("receiver_id", chatId.toString());
// messageJson.put("content", content);
// JSONObject messageJson = new JSONObject(); messageJson.put("message_type", messageType);
// messageJson.put("action", "send_message");
// messageJson.put("receiver_type", receiverType); // "private"/"group"/"channel" if (!attachmentsArray.isEmpty()) {
// messageJson.put("receiver_id", chatId.toString()); // در private = chat_id messageJson.put("attachments", attachmentsArray);
// messageJson.put("content", content); }
// messageJson.put("message_type", messageType);
// if (attachmentsArray.length() > 0) { JSONObject response = sendWithResponse(messageJson);
// messageJson.put("attachments", attachmentsArray); if (response != null && response.getString("status").equals("success")) {
// } System.out.println("✅ Message sent! ID: " + response.getJSONObject("data").getString("message_id"));
// } else {
// JSONObject response = sendWithResponse(messageJson); System.out.println("❌ Failed to send message: " + (response != null ? response.getString("message") : "No response"));
// if (response != null && response.getString("status").equals("success")) { }
// System.out.println("✅ Message sent! ID: " + response.getJSONObject("data").getString("message_id")); }
// } else {
// System.out.println("❌ Failed to send message: " + (response != null ? response.optString("message","No message") : "No response")); public void refreshContactList() {
// }
// }
//
private void refreshContactList() {
JSONObject req = new JSONObject(); JSONObject req = new JSONObject();
req.put("action", "get_contact_list"); req.put("action", "get_contact_list");
req.put("user_id", Session.currentUser.getString("user_id")); req.put("user_id", Session.currentUser.getString("user_id"));
@@ -3915,18 +3860,6 @@ public class ActionHandler {
replyLabel = "↪️ Reply to " + repliedSender + ": \"" + repliedContent + "\""; replyLabel = "↪️ Reply to " + repliedSender + ": \"" + repliedContent + "\"";
} }
JSONArray atts = msg.optJSONArray("attachments");
if (atts != null && atts.length() > 0) {
System.out.println(" 📎 " + atts.length() + " attachment(s)");
for (int a = 0; a < atts.length(); a++) {
JSONObject att = atts.getJSONObject(a);
String fn = att.optString("file_name", "(unnamed)");
long sz = att.optLong("file_size", 0);
System.out.printf(" - #%d %s (%s)\n", a + 1, fn, humanSize(sz));
}
}
JSONArray reactions = msg.optJSONArray("reactions"); JSONArray reactions = msg.optJSONArray("reactions");
if (reactions != null && !reactions.isEmpty()) { if (reactions != null && !reactions.isEmpty()) {
System.out.print(" 💬 Reactions: "); System.out.print(" 💬 Reactions: ");
@@ -3961,7 +3894,7 @@ public class ActionHandler {
} }
if(input.equalsIgnoreCase("S")){ if(input.equalsIgnoreCase("S")){
sendMessageInteractive(chat.getId(), chat.getType()); sendMessage(chat.getId(), chat.getType());
} }
try { try {
int index = Integer.parseInt(input); int index = Integer.parseInt(input);
@@ -3976,10 +3909,6 @@ public class ActionHandler {
boolean isSender = senderId.toString().equals(Session.currentUser.getString("internal_uuid")); boolean isSender = senderId.toString().equals(Session.currentUser.getString("internal_uuid"));
boolean isChannel = chat.getType().equals("channel"); boolean isChannel = chat.getType().equals("channel");
boolean isOwnerOrAdmin = chat.isOwner() || chat.isAdmin(); boolean isOwnerOrAdmin = chat.isOwner() || chat.isAdmin();
//for media
JSONArray atts = selected.optJSONArray("attachments");
boolean hasAttachments = (atts != null && atts.length() > 0);
System.out.println("\n🎯 Selected message by " + selected.getString("sender_name")); System.out.println("\n🎯 Selected message by " + selected.getString("sender_name"));
@@ -4005,9 +3934,6 @@ public class ActionHandler {
System.out.println("5. Delete"); System.out.println("5. Delete");
} }
} }
if (hasAttachments) {
System.out.println("D. Download attachment");
}
System.out.println("0. Back to message list"); System.out.println("0. Back to message list");
System.out.print("➤ Select an action: "); System.out.print("➤ Select an action: ");
@@ -4040,15 +3966,6 @@ public class ActionHandler {
System.out.println("❌ You are not allowed to delete this message."); System.out.println("❌ You are not allowed to delete this message.");
} }
case "0" -> {} case "0" -> {}
case "D", "d" -> {
if (hasAttachments) {
downloadAttachmentFlow(chat, selected);
} else {
System.out.println("🚫 No attachments to download.");
}
}
default -> System.out.println("❌ Invalid option."); default -> System.out.println("❌ Invalid option.");
} }
@@ -4059,122 +3976,6 @@ public class ActionHandler {
} }
public void editMessage(UUID messageId) { public void editMessage(UUID messageId) {
private void downloadAttachmentFlow(ChatEntry chat, JSONObject msg) {
JSONArray atts = msg.optJSONArray("attachments");
if (atts == null || atts.length() == 0) {
System.out.println("🚫 No attachments.");
return;
}
int idx = 0;
if (atts.length() > 1) {
System.out.print("Which attachment [1.." + atts.length() + "]? ");
try {
String ans = scanner.nextLine().trim();
if (!ans.isEmpty()) {
int n = Integer.parseInt(ans);
if (n >= 1 && n <= atts.length()) idx = n - 1;
}
} catch (Exception ignored) { idx = 0; }
}
JSONObject att = atts.getJSONObject(idx);
String mediaKeyStr = att.optString("media_key", "");
if (mediaKeyStr.isBlank()) {
System.out.println("❌ Attachment missing media_key.");
return;
}
UUID mediaKey = UUID.fromString(mediaKeyStr);
String rawName = att.optString("file_name", mediaKey.toString());
String fileName = sanitizeFileName(rawName);
long declaredSize = att.optLong("file_size", 0L);
// ~/Downloads/TeleSock/<Account>/<Chat>/
String accFolder = accountFolderName();
String chatFolder = chatFolderName(chat);
Path saveDir = Paths.get(System.getProperty("user.home"),
"Downloads", "TeleSock", accFolder, chatFolder);
System.out.println("👤 AccountFolder = " + accFolder);
System.out.println("💬 ChatFolder = " + chatFolder);
System.out.println("📁 SaveDir = " + saveDir);
try { Files.createDirectories(saveDir); }
catch (IOException e) {
System.out.println("❌ Cannot create folder: " + saveDir + " -> " + e.getMessage());
return;
}
DownloadsIndex di = Session.downloadsIndex;
if (di != null) {
Path existing = di.find(mediaKey);
if (existing != null) {
System.out.println("✅ Already downloaded: " + existing);
return;
}
}
Path target = uniquePath(saveDir, fileName);
TelegramClient.mediaBusy.set(true);
try {
Path saved = TelegramClient.getDownloader()
.download(mediaKey, saveDir, target.getFileName().toString());
long sizeToRecord = declaredSize > 0 ? declaredSize : Files.size(saved);
if (di != null) di.put(mediaKey, saved, sizeToRecord);
System.out.println("✅ Saved to: " + saved + " (" + humanSize(sizeToRecord) + ")");
} catch (Exception ex) {
System.out.println("❌ Download failed: " + ex.getMessage());
} finally {
TelegramClient.mediaBusy.set(false);
}
}
private static Path uniquePath(Path dir, String fileName) {
Path p = dir.resolve(fileName);
if (!Files.exists(p)) return p;
String name = fileName;
String ext = "";
int dot = fileName.lastIndexOf('.');
if (dot > 0 && dot < fileName.length()-1) {
name = fileName.substring(0, dot);
ext = fileName.substring(dot); // includes dot
}
int i = 1;
while (true) {
Path cand = dir.resolve(String.format("%s (%d)%s", name, i, ext));
if (!Files.exists(cand)) return cand;
i++;
}
}
private static String sanitizeFileName(String s) {
s = s.replace("\\", "/");
if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1);
s = s.replaceAll("[\\\\/:*?\"<>|]", "_");
if (s.equals(".") || s.equals("..") || s.isBlank()) s = "file";
return s;
}
private static String humanSize(long b) {
if (b <= 0) return "0 B";
String[] u = {"B","KB","MB","GB","TB"};
int i = (int) Math.floor(Math.log(b) / Math.log(1024));
if (i < 0) i = 0;
if (i >= u.length) i = u.length - 1;
double v = b / Math.pow(1024, i);
return String.format("%.1f %s", v, u[i]);
}
private void editMessage(UUID messageId) {
System.out.print("📝 Enter new content: "); System.out.print("📝 Enter new content: ");
String newContent = scanner.nextLine().trim(); String newContent = scanner.nextLine().trim();
@@ -4390,183 +4191,4 @@ public class ActionHandler {
} }
public void sendMessageInteractive(UUID receiverId, String receiverType) {
Scanner sc = new Scanner(System.in);
System.out.print("Type (TEXT / IMAGE / AUDIO): ");
String type = sc.nextLine().trim().toUpperCase();
while (!Set.of("TEXT","IMAGE","AUDIO").contains(type)) {
System.out.print("❌ Invalid. Try (TEXT / IMAGE / AUDIO): ");
type = sc.nextLine().trim().toUpperCase();
} }
System.out.print("Text (optional for media; empty = no caption): ");
String text = sc.nextLine();
if ("TEXT".equals(type)) {
sendTextMessage(receiverId, receiverType, text);
} else {
System.out.print("File path: ");
String path = sc.nextLine().trim();
File f = new File(path);
if (!f.isFile()) {
System.out.println("❌ File not found");
return;
}
try {
sendMediaMessage(receiverId, receiverType, type, f, text);
} catch (Exception e) {
e.printStackTrace();
System.out.println("❌ Media send failed: " + e.getMessage());
}
}
}
private void sendTextMessage(UUID receiverId, String receiverType, String content) {
JSONObject req = new JSONObject()
.put("action", "send_message")
.put("receiver_type", receiverType)
.put("receiver_id", receiverId.toString())
.put("message_type", "TEXT")
.put("content", content == null ? "" : content);
JSONObject resp = sendWithResponse(req);
if (resp != null && "success".equalsIgnoreCase(resp.optString("status"))) {
System.out.println("✅ Sent. id=" + resp.optJSONObject("data").optString("message_id",""));
} else {
System.out.println("❌ Failed: " + (resp != null ? resp.optString("message") : "no response"));
}
}
public void sendMediaMessage(UUID receiverId, String receiverType, String type /* IMAGE/AUDIO */, File file, String caption) {
if (file == null) {
System.out.println("❌ File is null");
return;
}
if (!file.exists()) {
System.out.println("❌ File not found: " + file.getAbsolutePath());
return;
}
if (file.isDirectory()) {
System.out.println("❌ Path is a directory, expected a file: " + file.getAbsolutePath());
return;
}
final UUID messageId = UUID.randomUUID();
try {
String mime = detectMime(file, type.toUpperCase());
if (mime == null) mime = type.equalsIgnoreCase("IMAGE") ? "image/*" : "audio/*";
JSONObject header = new JSONObject()
.put("message_id", messageId.toString())
.put("sender_id", TelegramClient.loggedInUserId.toString())
.put("receiver_type", receiverType) // private|group|channel
.put("receiver_id", receiverId.toString())
.put("message_type", type.toUpperCase()) // IMAGE | AUDIO
.put("file_name", file.getName())
.put("mime_type", mime)
.put("text", caption == null ? "" : caption);
byte[] headerBytes = header.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
long contentLen = file.length();
BlockingQueue<JSONObject> q = new LinkedBlockingQueue<>(1);
TelegramClient.pendingResponses.put(messageId.toString(), q);
try {
outBin.write("MEDIA\n".getBytes(java.nio.charset.StandardCharsets.US_ASCII));
outBin.flush();
// 2) binary frame: magic + headerLen + header + contentLen + content
outBin.writeInt(0x4D444D31); // "MDM1"
outBin.writeInt(headerBytes.length); // headerLen (int)
outBin.write(headerBytes); // header
outBin.writeLong(contentLen); // contentLen (long)
try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) {
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
outBin.write(buf, 0, n);
}
}
outBin.flush();
JSONObject ack = q.poll(20, java.util.concurrent.TimeUnit.SECONDS);
if (ack == null) {
System.out.println("❌ Media ACK timeout for " + messageId);
return;
}
String status = ack.optString("status", "error");
if ("success".equalsIgnoreCase(status)) {
System.out.println("✅ Media sent. id=" + ack.optString("message_id") +
" url=" + ack.optString("file_url"));
} else {
System.out.println("❌ Media failed: " + ack.optString("message"));
}
} finally {
TelegramClient.pendingResponses.remove(messageId.toString());
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("❌ sendMediaMessage error: " + e.getMessage());
}
}
private static String detectMime(File f, String typeUpper /* IMAGE or AUDIO */) {
try {
String m = java.nio.file.Files.probeContentType(f.toPath());
if (m != null) return m;
} catch (Exception ignored) {}
String name = f.getName().toLowerCase();
if (name.endsWith(".png")) return "image/png";
if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg";
if (name.endsWith(".gif")) return "image/gif";
if (name.endsWith(".mp3")) return "audio/mpeg";
if (name.endsWith(".wav")) return "audio/wav";
if (name.endsWith(".ogg")) return "audio/ogg";
return typeUpper.equals("IMAGE") ? "image/*" : "audio/*";
}
private static String safeName(String s) {
if (s == null) return "unknown";
s = s.replace("\\", "/");
if (s.contains("/")) s = s.substring(s.lastIndexOf('/') + 1);
s = s.replaceAll("[\\\\/:*?\"<>|]", "_").trim();
if (s.isEmpty() || s.equals(".") || s.equals("..")) s = "unknown";
return s;
}
private static String accountFolderName() {
JSONObject me = Session.currentUser;
String acc = me.optString("username",
me.optString("user_id",
me.optString("profile_name",
me.optString("internal_uuid", "me"))));
return safeName(acc);
}
private static String chatFolderName(ChatEntry chat) {
String name = chat.getName();
if (name == null || name.isBlank()) {
name = chat.getDisplayId() != null && !chat.getDisplayId().isBlank()
? chat.getDisplayId()
: String.valueOf(chat.getId());
}
return safeName(name);
}
}
@@ -8,9 +8,6 @@ import org.to.telegramfinalproject.UI.MainController;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.time.LocalDateTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@@ -18,7 +15,6 @@ import java.util.concurrent.BlockingQueue;
public class IncomingMessageListener implements Runnable { public class IncomingMessageListener implements Runnable {
private final BufferedReader in; private final BufferedReader in;
private volatile boolean running = true;
public enum UIMode { CONSOLE, UI } public enum UIMode { CONSOLE, UI }
private final UIMode uiMode; // runtime mode private final UIMode uiMode; // runtime mode
@@ -40,47 +36,16 @@ public class IncomingMessageListener implements Runnable {
try { try {
System.out.println("👂 Real-Time Listener started."); System.out.println("👂 Real-Time Listener started.");
while (running) { String line;
if (TelegramClient.mediaBusy.get()) { while ((line = in.readLine()) != null) {
try { Thread.sleep(15); } catch (InterruptedException ignored) {}
continue;
}
if (!in.ready()) {
try { Thread.sleep(10); } catch (InterruptedException ignored) {}
continue;
}
String line = in.readLine();
if (line == null) {
break;
}
if (line.isBlank()) continue;
final JSONObject response;
try {
response = new JSONObject(line);
} catch (Exception badJson) {
System.out.println("⚠️ [Listener] Non-JSON line ignored: " + line);
continue;
}
JSONObject response = new JSONObject(line);
System.out.println("📥 Received raw line: " + line); System.out.println("📥 Received raw line: " + line);
// --- Media ACK routing by message_id --- //if it has reqID answer
String mid = response.optString("message_id", "");
if (!mid.isEmpty()) {
BlockingQueue<JSONObject> q = TelegramClient.pendingResponses.get(mid);
if (q != null) {
q.put(response);
continue;
}
}
// --- General request_id response routing ---
if (response.has("request_id")) { if (response.has("request_id")) {
String requestId = response.optString("request_id", ""); String requestId = response.getString("request_id");
System.out.println("📬 Response with request_id: " + requestId); System.out.println("📬 Response with request_id: " + requestId);
System.out.println("📬 Full response: " + response.toString(2)); System.out.println("📬 Full response: " + response.toString(2));
@@ -91,12 +56,15 @@ public class IncomingMessageListener implements Runnable {
System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue..."); System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue...");
TelegramClient.responseQueue.put(response); TelegramClient.responseQueue.put(response);
} }
continue; continue;
} }
// --- Real-time actions ---
//if it has action check it
if (response.has("action")) { if (response.has("action")) {
String action = response.optString("action", ""); String action = response.getString("action");
System.out.println("🎯 [Listener] Action received: " + response.toString(2)); System.out.println("🎯 [Listener] Action received: " + response.toString(2));
System.out.println("🎯 Received action: " + action); System.out.println("🎯 Received action: " + action);
@@ -105,12 +73,11 @@ public class IncomingMessageListener implements Runnable {
} else { } else {
TelegramClient.responseQueue.put(response); TelegramClient.responseQueue.put(response);
} }
} else if (response.has("status") && response.has("message")) { } else if (response.has("status") && response.has("message")) {
// General success/error TelegramClient.responseQueue.put(response); // general answer
TelegramClient.responseQueue.put(response);
} else { } else {
// Fallback TelegramClient.responseQueue.put(response); // fallback
TelegramClient.responseQueue.put(response);
} }
} }
@@ -127,9 +94,7 @@ public class IncomingMessageListener implements Runnable {
"update_group_or_channel", "chat_deleted", "update_group_or_channel", "chat_deleted",
"blocked_by_user", "unblocked_by_user", "message_seen", "blocked_by_user", "unblocked_by_user", "message_seen",
"removed_from_group", "removed_from_channel", "removed_from_group", "removed_from_channel",
"became_admin", "removed_admin", "ownership_transferred", "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted","chat_updated" -> true;
"admin_permissions_updated", "created_private_chat",
"message_reacted", "message_unreacted" , "chat_updated"-> true;
default -> false; default -> false;
}; };
} }
@@ -148,11 +113,9 @@ public class IncomingMessageListener implements Runnable {
System.out.println("🔄 Chat list changed. Updating..."); System.out.println("🔄 Chat list changed. Updating...");
Session.forceRefreshChatList = true; Session.forceRefreshChatList = true;
String chatId = msg.optString("chat_id", ""); String chatId = msg.getString("chat_id");
String chatType = msg.optString("chat_type", ""); String chatType = msg.getString("chat_type");
if (!chatId.isBlank() && !chatType.isBlank()) {
ActionHandler.requestChatInfo(chatId, chatType); ActionHandler.requestChatInfo(chatId, chatType);
}
if (action.equals("removed_from_group") || action.equals("removed_from_channel") || action.equals("chat_deleted")) { if (action.equals("removed_from_group") || action.equals("removed_from_channel") || action.equals("chat_deleted")) {
System.out.println("🚫 You were removed from the chat or chat was deleted. Exiting..."); System.out.println("🚫 You were removed from the chat or chat was deleted. Exiting...");
@@ -235,136 +198,78 @@ public class IncomingMessageListener implements Runnable {
System.out.println("\n❓ Unknown real-time action: " + action); System.out.println("\n❓ Unknown real-time action: " + action);
System.out.println(msg.toString(2)); System.out.println(msg.toString(2));
} }
default -> displayRealTimeMessage(action, msg);
} }
System.out.print(">> "); System.out.print(">> ");
} }
// private void updateLastMessageTime(JSONObject msg) {
// try {
// UUID chatUUID = UUID.fromString(msg.getString("chat_id"));
// String newTime = msg.optString("last_message_time", null);
//
// Session.chatList.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst()
// .ifPresent(chat -> {
// chat.setLastMessageTime(newTime);
// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
// });
//
// Session.activeChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst()
// .ifPresent(chat -> {
// chat.setLastMessageTime(newTime);
// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
// });
//
// Session.archivedChats.stream().filter(chat -> chat.getId().equals(chatUUID)).findFirst()
// .ifPresent(chat -> {
// chat.setLastMessageTime(newTime);
// System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
// });
//
// Session.chatList.sort((c1, c2) -> {
// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
// if (c1.getLastMessageTime() == null) return 1;
// if (c2.getLastMessageTime() == null) return -1;
// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime());
// });
// Session.activeChats.sort((c1, c2) -> {
// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
// if (c1.getLastMessageTime() == null) return 1;
// if (c2.getLastMessageTime() == null) return -1;
// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime());
// });
// Session.archivedChats.sort((c1, c2) -> {
// if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
// if (c1.getLastMessageTime() == null) return 1;
// if (c2.getLastMessageTime() == null) return -1;
// return c2.getLastMessageTime().compareTo(c1.getLastMessageTime());
// });
//
// if (Session.inChatListMenu) {
// ActionHandler.displayChatList();
// System.out.print("Select a chat by number: ");
// }
//
// } catch (Exception e) {
// System.out.println("❌ Failed to update last message time: " + e.getMessage());
// }
// }
private void updateLastMessageTime(JSONObject msg) { private void updateLastMessageTime(JSONObject msg) {
try { try {
UUID chatUUID = UUID.fromString(msg.getString("chat_id")); UUID chatUUID = UUID.fromString(msg.getString("chat_id"));
String newTime = msg.optString("last_message_time", null); String newTime = msg.optString("last_message_time", null);
if (newTime == null || newTime.isBlank()) return;
updateOneList(Session.chatList, chatUUID, newTime); Session.chatList.stream()
updateOneList(Session.activeChats, chatUUID, newTime); .filter(chat -> chat.getId().equals(chatUUID))
updateOneList(Session.archivedChats, chatUUID, newTime); .findFirst()
.ifPresent(chat -> {
chat.setLastMessageTime(newTime);
System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
});
Session.activeChats.stream()
.filter(chat -> chat.getId().equals(chatUUID))
.findFirst()
.ifPresent(chat -> {
chat.setLastMessageTime(newTime);
System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
});
Session.archivedChats.stream()
.filter(chat -> chat.getId().equals(chatUUID))
.findFirst()
.ifPresent(chat -> {
chat.setLastMessageTime(newTime);
System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
});
Session.chatList.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending
});
Session.activeChats.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending
});
Session.archivedChats.sort((c1, c2) -> {
if (c1.getLastMessageTime() == null && c2.getLastMessageTime() == null) return 0;
if (c1.getLastMessageTime() == null) return 1;
if (c2.getLastMessageTime() == null) return -1;
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime()); // descending
});
sortByLastMessageTime(Session.chatList);
sortByLastMessageTime(Session.activeChats);
sortByLastMessageTime(Session.archivedChats);
if (Session.inChatListMenu) { if (Session.inChatListMenu) {
ActionHandler.displayChatList(); ActionHandler.displayChatList();
System.out.print("Select a chat by number: "); System.out.print("Select a chat by number: ");
} }
} catch (Exception e) { } catch (Exception e) {
System.out.println("❌ Failed to update last message time: " + e.getMessage()); System.out.println("❌ Failed to update last message time: " + e.getMessage());
} }
} }
private void updateOneList(List<ChatEntry> list, UUID chatUUID, String newTime) {
if (list == null) return;
for (ChatEntry chat : list) {
if (chatUUID.equals(chat.getId())) { // ✅ internal UUID
chat.setLastMessageTime(newTime);
System.out.println("✅ Updated last message time for chat: " + chat.getDisplayId());
break;
}
}
}
private static void sortByLastMessageTime(java.util.List<ChatEntry> list) {
if (list == null) return;
list.sort((a, b) -> {
var ta = parseTs(String.valueOf(a.getLastMessageTime()));
var tb = parseTs(String.valueOf(b.getLastMessageTime()));
if (ta == null && tb == null) return 0;
if (ta == null) return 1;
if (tb == null) return -1;
return tb.compareTo(ta);
});
for (int i = 0; i < list.size(); i++) {
if (list.get(i).isSavedMessages()) {
list.add(0, list.remove(i));
break;
}
}
}
private static java.time.LocalDateTime parseTs(String s) {
if (s == null) return null;
s = s.trim();
if (s.isEmpty() || s.equalsIgnoreCase("null")) return null;
try { return java.time.OffsetDateTime.parse(s).toLocalDateTime(); } catch (Exception ignore) {}
try { return java.time.LocalDateTime.parse(s, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME); } catch (Exception ignore) {}
return null;
}
private void handleAdminRoleChanged(JSONObject data) throws IOException { private void handleAdminRoleChanged(JSONObject data) throws IOException {
String chatType = data.optString("chat_type", ""); String chatType = data.getString("chat_type");
String chatId = data.optString("group_id", String chatId = data.optString("group_id", data.optString("channel_id", data.optString("chat_id", null)));
data.optString("channel_id", data.optString("chat_id", "")));
if (chatId.isBlank()) { if (chatId == null) {
System.out.println("⚠️ No valid ID found in real-time data: " + data.toString(2)); System.out.println("⚠️ No valid ID found in real-time data: " + data.toString(2));
return; return;
} }
@@ -372,10 +277,11 @@ public class IncomingMessageListener implements Runnable {
System.out.println("\n🔄 Your admin status changed. Updating chat info..."); System.out.println("\n🔄 Your admin status changed. Updating chat info...");
try { try {
JSONObject chatInfoReq = new JSONObject() // 1. get chat info
.put("action", "get_chat_info") JSONObject chatInfoReq = new JSONObject();
.put("receiver_id", chatId) chatInfoReq.put("action", "get_chat_info");
.put("receiver_type", chatType); chatInfoReq.put("receiver_id", chatId);
chatInfoReq.put("receiver_type", chatType);
System.out.println("📤 Sending get_chat_info: " + chatInfoReq); System.out.println("📤 Sending get_chat_info: " + chatInfoReq);
JSONObject chatInfoResp = ActionHandler.sendWithResponse(chatInfoReq); JSONObject chatInfoResp = ActionHandler.sendWithResponse(chatInfoReq);
JSONObject chatData = chatInfoResp.getJSONObject("data"); JSONObject chatData = chatInfoResp.getJSONObject("data");
@@ -401,17 +307,21 @@ public class IncomingMessageListener implements Runnable {
Session.currentChatEntry = chat; Session.currentChatEntry = chat;
}); });
// 2. get permission
JSONObject permissionReq = new JSONObject(); JSONObject permissionReq = new JSONObject();
if (chatType.equalsIgnoreCase("group")) { if (chatType.equalsIgnoreCase("group")) {
permissionReq.put("action", "get_group_permissions").put("group_id", chatId); permissionReq.put("action", "get_group_permissions");
permissionReq.put("group_id", chatId);
} else { } else {
permissionReq.put("action", "get_channel_permissions").put("channel_id", chatId); permissionReq.put("action", "get_channel_permissions");
permissionReq.put("channel_id", chatId);
} }
JSONObject permissionResp = ActionHandler.sendWithResponse(permissionReq); JSONObject permissionResp = ActionHandler.sendWithResponse(permissionReq);
JSONObject perm = permissionResp.getJSONObject("data"); JSONObject perm = permissionResp.getJSONObject("data");
entry.ifPresent(chat -> chat.setPermissions(perm)); entry.ifPresent(chat -> chat.setPermissions(perm));
// 3. set currentChatId
Session.currentChatId = chatUUID.toString(); Session.currentChatId = chatUUID.toString();
System.out.println("🧪 Checking refresh conditions..."); System.out.println("🧪 Checking refresh conditions...");
@@ -434,11 +344,20 @@ public class IncomingMessageListener implements Runnable {
} }
} }
private void displayRealTimeMessage(String action, JSONObject msg) { private void displayRealTimeMessage(String action, JSONObject msg) {
switch (action) { switch (action) {
case "new_message" -> { case "new_message" -> {
String senderName = msg.optString("sender_name","Unknown"); String senderName = msg.optString("sender_name","Unknown");
String content = msg.optString("content",""); String content = msg.optString("content","(empty)");
String sendAt = msg.optString("send_at","-"); String sendAt = msg.optString("send_at","-");
String chatId = msg.optString("receiver_id", msg.optString("chat_id","")); String chatId = msg.optString("receiver_id", msg.optString("chat_id",""));
String kind = msg.optString("kind","plain"); String kind = msg.optString("kind","plain");
@@ -457,47 +376,47 @@ public class IncomingMessageListener implements Runnable {
Session.currentChatId != null && Session.currentChatId.equals(chatId); Session.currentChatId != null && Session.currentChatId.equals(chatId);
if (isInCurrentChat) { if (isInCurrentChat) {
if (content.isBlank()) content = "(no content)"; // برای مدیا بدون کپشن
System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")"); System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")");
} else { } else {
String preview = content.isBlank() ? "[media]" : content; System.out.println("💬 Message from " + senderName + ": " + prefix + content);
System.out.println("💬 Message from " + senderName + ": " + prefix + preview);
Session.forceRefreshChatList = true; Session.forceRefreshChatList = true;
} }
} }
case "message_edited" -> { case "message_edited" -> {
System.out.println("\n✏️ Message Edited:"); System.out.println("\n✏️ Message Edited:");
System.out.println("ID: " + msg.optString("message_id","")); System.out.println("ID: " + msg.getString("message_id"));
System.out.println("New Content: " + msg.optString("new_content","")); System.out.println("New Content: " + msg.getString("new_content"));
System.out.println("Edit Time: " + msg.optString("edited_at","")); System.out.println("Edit Time: " + msg.getString("edited_at"));
} }
case "message_deleted_global" -> { case "message_deleted_global" -> {
System.out.println("\n🗑️ Message Deleted:"); System.out.println("\n🗑️ Message Deleted:");
System.out.println("Message ID: " + msg.optString("message_id","")); System.out.println("Message ID: " + msg.getString("message_id"));
} }
case "message_reacted", "message_unreacted" -> { case "message_reacted", "message_unreacted" -> {
String mid = msg.optString("message_id",""); String mid = msg.getString("message_id");
String emoji = msg.optString("emoji",""); String emoji = msg.getString("emoji");
JSONObject counts = msg.optJSONObject("counts");
int n = msg.optInt("count_for_emoji", 0); int n = msg.optInt("count_for_emoji", 0);
System.out.println("\n⭐ Reaction update on " + mid + " : " + emoji + "" + n); System.out.println("\n⭐ Reaction update on " + mid + " : " + emoji + "" + n);
} }
case "user_status_changed" -> { case "user_status_changed" -> {
System.out.println("\n🔄 User Status Changed:"); System.out.println("\n🔄 User Status Changed:");
System.out.println("User: " + msg.optString("user_id","")); System.out.println("User: " + msg.getString("user_id"));
System.out.println("Status: " + msg.optString("status","")); System.out.println("Status: " + msg.getString("status"));
} }
case "blocked_by_user" -> { case "blocked_by_user" -> {
System.out.println("\n⛔ You were blocked by user: " + msg.optString("blocker_id","")); System.out.println("\n⛔ You were blocked by user: " + msg.getString("blocker_id"));
} }
case "unblocked_by_user" -> { case "unblocked_by_user" -> {
System.out.println("\n✅ You were unblocked by user: " + msg.optString("unblocker_id","")); System.out.println("\n✅ You were unblocked by user: " + msg.getString("unblocker_id"));
} }
case "message_seen" -> { case "message_seen" -> {
System.out.println("\n👁️ Your message was seen:"); System.out.println("\n👁️ Your message was seen:");
System.out.println("Message ID: " + msg.optString("message_id","")); System.out.println("Message ID: " + msg.getString("message_id"));
System.out.println("Seen at: " + msg.optString("seen_at","")); System.out.println("Seen at: " + msg.getString("seen_at"));
} }
default -> { default -> {
System.out.println("\n❓ Unknown real-time action: " + action); System.out.println("\n❓ Unknown real-time action: " + action);
@@ -508,9 +427,6 @@ public class IncomingMessageListener implements Runnable {
private static LocalDateTime parseIsoFlexible(String iso) { private static LocalDateTime parseIsoFlexible(String iso) {
if (iso == null || iso.isBlank()) return null; if (iso == null || iso.isBlank()) return null;
try { return LocalDateTime.parse(iso); } catch (Exception ignore) {} try { return LocalDateTime.parse(iso); } catch (Exception ignore) {}
@@ -128,7 +128,10 @@ package org.to.telegramfinalproject.Client;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.*; import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket; import java.net.Socket;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
@@ -137,11 +140,9 @@ import java.util.UUID;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class TelegramClient { public class TelegramClient {
private static final String SERVER_HOST = "localhost"; private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8080;
private static final int SERVER_PORT = 8000; private static final int SERVER_PORT = 8000;
private static TelegramClient instance; private static TelegramClient instance;
@@ -157,13 +158,6 @@ public class TelegramClient {
public static final BlockingQueue<JSONObject> responseQueue = new LinkedBlockingQueue<>(); public static final BlockingQueue<JSONObject> responseQueue = new LinkedBlockingQueue<>();
public static final Map<String, BlockingQueue<JSONObject>> pendingResponses = new ConcurrentHashMap<>(); public static final Map<String, BlockingQueue<JSONObject>> pendingResponses = new ConcurrentHashMap<>();
public static UUID loggedInUserId = null; public static UUID loggedInUserId = null;
public static final Map<String, BlockingQueue<JSONObject>> pendingResponses = new ConcurrentHashMap<>();
private DataInputStream inBin; // NEW
private static SocketMediaDownloader downloader; // NEW
public static final AtomicBoolean mediaBusy = new AtomicBoolean(false); //
private DownloadsIndex downloadIndex;
private static TelegramClient instance;
private volatile boolean listenerStarted = false; private volatile boolean listenerStarted = false;
@@ -174,14 +168,8 @@ public class TelegramClient {
public static synchronized TelegramClient getInstance() { public static synchronized TelegramClient getInstance() {
if (instance == null) instance = new TelegramClient(); if (instance == null) instance = new TelegramClient();
public static SocketMediaDownloader getDownloader() {
return downloader;
}
public static TelegramClient getInstance() {
return instance; return instance;
} }
private DataOutputStream outBin;
// public void startConsole() { // public void startConsole() {
// try { // try {
@@ -196,18 +184,6 @@ public class TelegramClient {
public void startConsole() { public void startConsole() {
try { try {
socket = new Socket(SERVER_HOST, SERVER_PORT);
InputStream rawIn = socket.getInputStream();
OutputStream rawOut = socket.getOutputStream();
in = new BufferedReader(new InputStreamReader(rawIn, StandardCharsets.UTF_8));
out = new PrintWriter(new OutputStreamWriter(rawOut, StandardCharsets.UTF_8), true);
inBin = new DataInputStream(rawIn);
outBin = new DataOutputStream(rawOut);
downloader = new SocketMediaDownloader(out, inBin, outBin);
System.out.println("✅ Connected to Telegram Server");
handler = new ActionHandler(out, in, outBin, scanner);
connectIfNeeded(); connectIfNeeded();
initHandlerIfNeeded(); initHandlerIfNeeded();
startListenerOnce(IncomingMessageListener.UIMode.CONSOLE); // ← کنسول startListenerOnce(IncomingMessageListener.UIMode.CONSOLE); // ← کنسول
@@ -293,7 +269,7 @@ public class TelegramClient {
UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid")); UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid"));
loggedInUserId = internalId; loggedInUserId = internalId;
this.downloadIndex = DownloadIndexRegistry.forAccount(internalId);
handler.userMenu(internalId); handler.userMenu(internalId);
} else { } else {
System.out.println("❌ Login failed."); System.out.println("❌ Login failed.");
@@ -335,4 +311,3 @@ public class TelegramClient {
} }
} }
@@ -1,9 +1,7 @@
package org.to.telegramfinalproject.Database; package org.to.telegramfinalproject.Database;
import org.to.telegramfinalproject.Models.FileAttachment; import org.to.telegramfinalproject.Models.FileAttachment;
import org.to.telegramfinalproject.Models.MediaRow;
import org.to.telegramfinalproject.Models.Message; import org.to.telegramfinalproject.Models.Message;
import org.to.telegramfinalproject.Utils.ChannelPermissionUtil;
import java.sql.*; import java.sql.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -68,134 +66,6 @@ public class MessageDatabase {
} }
} }
public static boolean insertMessageTx(Connection conn, UUID messageId, UUID senderId, UUID receiverId,
String receiverType, String content, String messageType) throws SQLException {
String sql = "INSERT INTO messages (message_id, sender_id, receiver_type, receiver_id, content, message_type) " +
"VALUES (?, ?, ?, ?, ?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setObject(1, messageId);
ps.setObject(2, senderId);
ps.setString(3, receiverType);
ps.setObject(4, receiverId);
if (content == null || content.isBlank()) ps.setNull(5, java.sql.Types.VARCHAR); else ps.setString(5, content);
ps.setString(6, messageType);
return ps.executeUpdate() > 0;
}
}
public static boolean insertAttachmentsTx(Connection conn, UUID messageId, List<FileAttachment> attachments) throws SQLException {
if (attachments == null || attachments.isEmpty()) return true;
final String sql = """
INSERT INTO message_attachments(
attachment_id, message_id,
file_url, file_type, file_name, file_size, mime_type,
width, height, duration_seconds, thumbnail_url,
media_key, storage_path
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
""";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
for (FileAttachment att : attachments) {
if (att == null) throw new IllegalArgumentException("Attachment is null");
UUID attachmentId = att.getAttachmentId() != null ? att.getAttachmentId() : UUID.randomUUID();
UUID mediaKey = att.getMediaKey() != null ? att.getMediaKey() : attachmentId; // ساده‌ترین حالت
String ft = att.getFileType();
if (!"IMAGE".equalsIgnoreCase(ft) && !"AUDIO".equalsIgnoreCase(ft)) {
throw new IllegalArgumentException("file_type must be IMAGE or AUDIO");
}
if (att.getStoragePath() == null || att.getStoragePath().isBlank()) {
throw new IllegalArgumentException("storage_path is required for socket downloads");
}
int i = 1;
ps.setObject(i++, attachmentId);
ps.setObject(i++, messageId);
//file url (display link)
if (att.getFileUrl() == null || att.getFileUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR);
else ps.setString(i++, att.getFileUrl());
ps.setString(i++, ft.toUpperCase());
ps.setString(i++, att.getFileName());
if (att.getFileSize() == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, att.getFileSize());
if (att.getMimeType() == null) ps.setNull(i++, java.sql.Types.VARCHAR); else ps.setString(i++, att.getMimeType());
if (att.getWidth() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getWidth());
if (att.getHeight() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getHeight());
if (att.getDurationSeconds() == null) ps.setNull(i++, java.sql.Types.INTEGER); else ps.setInt(i++, att.getDurationSeconds());
if (att.getThumbnailUrl() == null || att.getThumbnailUrl().isBlank()) ps.setNull(i++, java.sql.Types.VARCHAR);
else ps.setString(i++, att.getThumbnailUrl());
ps.setObject(i++, mediaKey);
ps.setString(i++, att.getStoragePath());
ps.addBatch();
att.setAttachmentId(attachmentId);
att.setMediaKey(mediaKey);
}
ps.executeBatch();
return true;
}
}
public static boolean saveMessageWithOptionalAttachments(
UUID messageId, UUID senderId, UUID receiverId,
String receiverType, String content, String messageType,
List<FileAttachment> attachments
) {
Connection conn = null;
try {
conn = ConnectionDb.connect();
conn.setAutoCommit(false);
boolean isText = "TEXT".equalsIgnoreCase(messageType);
boolean isImage = "IMAGE".equalsIgnoreCase(messageType);
boolean isAudio = "AUDIO".equalsIgnoreCase(messageType);
if (!isText && !isImage && !isAudio) {
throw new IllegalArgumentException("messageType must be TEXT, IMAGE, or AUDIO");
}
if (isText) {
if (attachments != null && !attachments.isEmpty())
throw new IllegalArgumentException("TEXT must not have attachments");
if (content == null || content.isBlank())
throw new IllegalArgumentException("TEXT must have non-empty content");
} else {
if (attachments == null || attachments.isEmpty())
throw new IllegalArgumentException("Non-TEXT must have at least one attachment");
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.toUpperCase());
if (!isText) insertAttachmentsTx(conn, messageId, attachments);
conn.commit();
return true;
} catch (Exception e) {
if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {}
e.printStackTrace();
return false;
} finally {
if (conn != null) {
try { conn.setAutoCommit(true); } catch (SQLException ignored) {}
try { conn.close(); } catch (SQLException ignored) {}
}
}
}
public static void markGloballyDeleted(UUID chatId) { public static void markGloballyDeleted(UUID chatId) {
String sql = "UPDATE messages SET is_deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'"; String sql = "UPDATE messages SET is_deleted_globally = true WHERE receiver_id = ? AND receiver_type = 'private'";
try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) { try (Connection conn = ConnectionDb.connect(); PreparedStatement ps = conn.prepareStatement(sql)) {
@@ -684,34 +554,30 @@ public class MessageDatabase {
public static List<FileAttachment> getAttachments(UUID messageId) { public static List<FileAttachment> getAttachments(UUID messageId) {
List<FileAttachment> attachments = new ArrayList<>(); List<FileAttachment> attachments = new ArrayList<>();
String sql = "SELECT file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url " + String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?";
"FROM message_attachments WHERE message_id = ? ORDER BY uploaded_at";
try (Connection conn = ConnectionDb.connect(); try (Connection conn = ConnectionDb.connect();
PreparedStatement stmt = conn.prepareStatement(sql)) { PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setObject(1, messageId); stmt.setObject(1, messageId);
ResultSet rs = stmt.executeQuery(); ResultSet rs = stmt.executeQuery();
while (rs.next()) { while (rs.next()) {
attachments.add(new FileAttachment( attachments.add(new FileAttachment(
rs.getString("file_url"), rs.getString("file_url"),
rs.getString("file_type"), rs.getString("file_type")
rs.getString("file_name"),
(Long) rs.getObject("file_size"),
rs.getString("mime_type"),
(Integer) rs.getObject("width"),
(Integer) rs.getObject("height"),
(Integer) rs.getObject("duration_seconds"),
rs.getString("thumbnail_url")
)); ));
} }
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); e.printStackTrace();
} }
return attachments; return attachments;
} }
public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) { public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) {
String sql = """ String sql = """
SELECT MAX(send_at) FROM messages SELECT MAX(send_at) FROM messages
@@ -1297,173 +1163,6 @@ public class MessageDatabase {
} }
//
// public static MediaRow findMediaByKey(UUID mediaKey) throws SQLException {
// final String sql = """
// SELECT a.message_id, a.storage_path, a.file_name, a.mime_type, a.file_size,
// m.receiver_type, m.receiver_id, m.sender_id
// FROM message_attachments a
// JOIN messages m ON m.message_id = a.message_id
// WHERE a.media_key = ?
// """;
// try (Connection c = ConnectionDb.connect();
// PreparedStatement ps = c.prepareStatement(sql)) {
// ps.setObject(1, mediaKey);
// try (ResultSet rs = ps.executeQuery()) {
// if (!rs.next()) return null;
// MediaRow mr = new MediaRow();
// mr.messageId = (UUID) rs.getObject(1);
// mr.storagePath = rs.getString(2);
// mr.fileName = rs.getString(3);
// mr.mimeType = rs.getString(4);
// mr.fileSize = rs.getLong(5);
// mr.receiverType= rs.getString(6);
// mr.receiverId = (UUID) rs.getObject(7);
// mr.senderId = (UUID) rs.getObject(8);
// return mr;
// }
// }
// }
// public static boolean canAccess(UUID requester, MediaRow mr) {
// if ("private".equals(mr.receiverType)) {
// return requester.equals(mr.senderId) || requester.equals(mr.receiverId);
// } else if ("group".equals(mr.receiverType)) {
// return GroupDatabase.isMember(mr.receiverId, requester);
// } else if ("channel".equals(mr.receiverType)) {
// return ChannelDatabase.isUserInChannel(mr.receiverId, requester);
// }
// return false;
// }
public static Map<UUID, List<MediaRow>> findAttachmentsForMessages(List<UUID> ids) throws SQLException {
Map<UUID, List<MediaRow>> map = new java.util.HashMap<>();
if (ids == null || ids.isEmpty()) return map;
// ساخت IN به‌صورت امن
String placeholders = ids.stream().map(x -> "?").collect(java.util.stream.Collectors.joining(","));
String sql = """
SELECT attachment_id, message_id, media_key, file_name, file_size, mime_type, file_type,
width, height, duration_seconds, thumbnail_url, file_url, storage_path
FROM message_attachments
WHERE message_id IN (""" + placeholders + ") ORDER BY uploaded_at ASC";
try (Connection c = ConnectionDb.connect();
PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1;
for (UUID id : ids) ps.setObject(i++, id);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
MediaRow a = new MediaRow();
a.attachmentId = (UUID) rs.getObject("attachment_id");
a.messageId = (UUID) rs.getObject("message_id");
a.mediaKey = (UUID) rs.getObject("media_key");
a.fileName = rs.getString("file_name");
long sz = rs.getLong("file_size");
a.fileSize = rs.wasNull() ? null : sz;
a.mimeType = rs.getString("mime_type");
a.fileType = rs.getString("file_type");
int w = rs.getInt("width");
a.width = rs.wasNull() ? null : w;
int h = rs.getInt("height");
a.height = rs.wasNull() ? null : h;
int d = rs.getInt("duration_seconds");
a.durationSeconds = rs.wasNull() ? null : d;
a.thumbnailUrl = rs.getString("thumbnail_url");
a.fileUrl = rs.getString("file_url");
a.storagePath = rs.getString("storage_path");
map.computeIfAbsent(a.messageId, k -> new java.util.ArrayList<>()).add(a);
}
}
}
return map;
}
public static MediaRow findMediaByKey(UUID mediaKey) throws SQLException {
String sql = """
SELECT
ma.attachment_id,
ma.message_id,
ma.media_key,
ma.file_name,
ma.file_size,
ma.mime_type,
ma.file_type,
ma.width,
ma.height,
ma.duration_seconds,
ma.thumbnail_url,
ma.file_url,
ma.storage_path,
m.receiver_type,
m.receiver_id,
m.sender_id
FROM message_attachments ma
JOIN messages m ON m.message_id = ma.message_id
WHERE ma.media_key = ?
LIMIT 1
""";
try (Connection c = ConnectionDb.connect();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setObject(1, mediaKey);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
MediaRow a = new MediaRow();
a.attachmentId = (UUID) rs.getObject("attachment_id");
a.messageId = (UUID) rs.getObject("message_id");
a.mediaKey = (UUID) rs.getObject("media_key");
a.fileName = rs.getString("file_name");
long sz = rs.getLong("file_size");
a.fileSize = rs.wasNull() ? null : sz; // MediaRow.fileSize = Long
a.mimeType = rs.getString("mime_type");
a.fileType = rs.getString("file_type");
int w = rs.getInt("width"); a.width = rs.wasNull() ? null : w;
int h = rs.getInt("height"); a.height = rs.wasNull() ? null : h;
int d = rs.getInt("duration_seconds"); a.durationSeconds = rs.wasNull() ? null : d;
a.thumbnailUrl = rs.getString("thumbnail_url");
a.fileUrl = rs.getString("file_url");
a.storagePath = rs.getString("storage_path");
a.receiverType = rs.getString("receiver_type");
a.receiverId = (UUID) rs.getObject("receiver_id");
a.senderId = (UUID) rs.getObject("sender_id");
return a;
}
}
}
public static boolean canAccess(UUID requester, MediaRow mr) {
if (requester == null || mr == null || mr.receiverType == null) return false;
// اختیاری: فرستنده همیشه مجاز
if (requester.equals(mr.senderId)) return true;
switch (mr.receiverType.toLowerCase(Locale.ROOT)) {
case "private":
// receiver_id در پیام‌های private = UUID چت خصوصی
return PrivateChatDatabase.isParticipant(mr.receiverId, requester);
case "group":
return GroupDatabase.isMember(mr.receiverId, requester);
case "channel":
return ChannelPermissionUtil.isUserInChannel(requester, mr.receiverId);
default:
return false;
}
}
public static Message getLastMessage(UUID targetId, String type) { public static Message getLastMessage(UUID targetId, String type) {
final String sql = final String sql =
"SELECT * FROM messages " + "SELECT * FROM messages " +
@@ -12,10 +12,6 @@ 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.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.*;
@@ -24,15 +20,6 @@ public class ClientHandler implements Runnable {
private final AuthService authService = new AuthService(); private final AuthService authService = new AuthService();
private User currentUser; 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) { public ClientHandler(Socket socket) {
this.socket = socket; this.socket = socket;
@@ -43,60 +30,11 @@ public class ClientHandler implements Runnable {
UUID userId = null; UUID userId = null;
try ( try (
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// InputStream rawIn = socket.getInputStream(); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
// 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; String inputLine;
while ((inputLine = readUtf8Line(bis)) != null) { while ((inputLine = in.readLine()) != 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); JSONObject requestJson = new JSONObject(inputLine);
String action = requestJson.getString("action"); String action = requestJson.getString("action");
ResponseModel response = null; ResponseModel response = null;
@@ -2337,13 +2275,6 @@ public class ClientHandler implements Runnable {
List<Message> messages = MessageDatabase.getMessagesForChat(chatId, chatType, currentUser.getInternal_uuid(), offset, limit); 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(); JSONArray result = new JSONArray();
for (Message m : messages) { for (Message m : messages) {
JSONObject obj = new JSONObject(); JSONObject obj = new JSONObject();
@@ -2411,26 +2342,6 @@ public class ClientHandler implements Runnable {
obj.put("reactions", new JSONArray(reactions)); 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); result.put(obj);
} }
@@ -2959,8 +2870,6 @@ public class ClientHandler implements Runnable {
userDatabase.updateLastSeen(userId); userDatabase.updateLastSeen(userId);
SessionManager.removeUser(userId); SessionManager.removeUser(userId);
} }
} catch (SQLException e) {
throw new RuntimeException(e);
} finally { } finally {
try { try {
if (currentUser != null) { if (currentUser != null) {
@@ -2986,606 +2895,9 @@ 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) { private ResponseModel handleSendMessage(JSONObject json) {
try { try {
if (currentUser == null) if (currentUser == null)
return new ResponseModel("error", "Unauthorized. Please login first."); return new ResponseModel("error", "Unauthorized. Please login first.");
@@ -3593,71 +2905,57 @@ public class ClientHandler implements Runnable {
UUID messageId = UUID.randomUUID(); UUID messageId = UUID.randomUUID();
UUID senderId = currentUser.getInternal_uuid(); UUID senderId = currentUser.getInternal_uuid();
String receiverType = json.getString("receiver_type"); String receiverType = json.getString("receiver_type");
UUID receiverId = UUID.fromString(json.getString("receiver_id")); 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 content = json.optString("content", "");
String messageType = json.optString("message_type", "TEXT"); String messageType = json.optString("message_type", "TEXT");
// Parse attachments boolean inserted = MessageDatabase.insertMessage(messageId, senderId, receiverId, receiverType, content, messageType);
List<FileAttachment> attachments = new ArrayList<>(); if (!inserted)
return new ResponseModel("error", "Failed to insert message.");
if (json.has("attachments")) { if (json.has("attachments")) {
JSONArray arr = json.getJSONArray("attachments"); JSONArray attachmentsArray = json.getJSONArray("attachments");
for (int i = 0; i < arr.length(); i++) { List<FileAttachment> attachments = new ArrayList<>();
JSONObject a = arr.getJSONObject(i);
for (int i = 0; i < attachmentsArray.length(); i++) {
JSONObject attJson = attachmentsArray.getJSONObject(i);
attachments.add(new FileAttachment( attachments.add(new FileAttachment(
a.optString("file_url",""), attJson.getString("file_url"),
a.optString("file_type","FILE"), attJson.getString("file_type")
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)
)); ));
} }
}
if ((content == null || content.isBlank()) && attachments.isEmpty()) { boolean attInserted = MessageDatabase.insertAttachments(messageId, attachments);
return new ResponseModel("error", "Empty message: no content or attachment."); if (!attInserted)
}
// 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."); return new ResponseModel("error", "Message inserted but failed to attach files.");
} }
}
conn.commit(); // Send real-time message
}
// Real-Time
Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now()); 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(); JSONObject chatPayload = new JSONObject();
chatPayload.put("action", "chat_updated"); chatPayload.put("action", "chat_updated");
@@ -3669,76 +2967,9 @@ public class ClientHandler implements Runnable {
RealTimeEventDispatcher.sendToUser(senderId, chatPayload); RealTimeEventDispatcher.sendToUser(senderId, chatPayload);
JSONObject payload = new JSONObject();
payload.put("action", "new_message");
JSONObject data = new JSONObject(); JSONObject data = new JSONObject();
data.put("id", messageId.toString()); data.put("message_id", messageId.toString());
data.put("sender_id", senderId.toString()); return new ResponseModel("success", "Message sent successfully.", data);
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) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@@ -3747,7 +2978,6 @@ public class ClientHandler implements Runnable {
} }
private List<UUID> getReceiversForChat(UUID receiverId, String receiverType) { private List<UUID> getReceiversForChat(UUID receiverId, String receiverType) {
switch (receiverType) { switch (receiverType) {
case "private": case "private":
@@ -40,7 +40,7 @@ public class LoginController {
visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty()); visiblePasswordField.textProperty().bindBidirectional(passwordField.textProperty());
try { try {
connection = new ClientConnection("localhost", 8000); connection = new ClientConnection("localhost", 8080);
} catch (Exception e) { } catch (Exception e) {
System.out.println("Could not connect to server: " + e.getMessage()); System.out.println("Could not connect to server: " + e.getMessage());
} }