Merge branch 'develop' into Main-UI
# Conflicts: # src/main/java/org/to/telegramfinalproject/Client/ActionHandler.java # src/main/java/org/to/telegramfinalproject/Client/IncomingMessageListener.java # src/main/java/org/to/telegramfinalproject/Client/TelegramClient.java # src/main/java/org/to/telegramfinalproject/Database/MessageDatabase.java # src/main/java/org/to/telegramfinalproject/Server/ClientHandler.java
This commit is contained in:
@@ -11,6 +11,10 @@ module org.to.telegramfinalproject {
|
||||
requires eu.hansolo.tilesfx;
|
||||
requires org.json;
|
||||
requires java.sql;
|
||||
requires java.desktop;
|
||||
requires spark.core;
|
||||
requires javax.servlet.api;
|
||||
requires mp3agic;
|
||||
opens org.to.telegramfinalproject to javafx.fxml;
|
||||
exports org.to.telegramfinalproject;
|
||||
exports org.to.telegramfinalproject.Client;
|
||||
|
||||
@@ -2,22 +2,18 @@ package org.to.telegramfinalproject.Client;
|
||||
|
||||
import org.json.JSONArray;
|
||||
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.ContactEntry;
|
||||
import org.to.telegramfinalproject.Models.SearchRequestModel;
|
||||
import org.to.telegramfinalproject.Models.SearchResultModel;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
public class ActionHandler {
|
||||
@@ -26,6 +22,7 @@ public class ActionHandler {
|
||||
private final Scanner scanner;
|
||||
public static volatile boolean forceExitChat = false;
|
||||
public static ActionHandler instance;
|
||||
private final DataOutputStream outBin;
|
||||
|
||||
//use for UI
|
||||
private volatile String lastStatus = "error"; // success | error
|
||||
@@ -57,12 +54,12 @@ public class ActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) {
|
||||
public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) {
|
||||
this.out = out;
|
||||
this.in = in;
|
||||
this.outBin = outBin;
|
||||
this.scanner = scanner;
|
||||
ActionHandler.instance = this;
|
||||
|
||||
}
|
||||
|
||||
public void login(String username , String password){
|
||||
@@ -545,6 +542,12 @@ public class ActionHandler {
|
||||
case "register":
|
||||
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 Archived = Session.currentUser.getJSONArray("archived_chat_list");
|
||||
JSONArray Active = Session.currentUser.getJSONArray("active_chat_list");
|
||||
@@ -659,7 +662,6 @@ public class ActionHandler {
|
||||
|
||||
|
||||
|
||||
|
||||
Session.activeChats = activeChats;
|
||||
Session.archivedChats = archivedChats;
|
||||
Session.chatList = chatList;
|
||||
@@ -1469,7 +1471,7 @@ public class ActionHandler {
|
||||
JSONObject m = messages.getJSONObject(i);
|
||||
String senderId = m.getString("sender_id");
|
||||
String senderName = m.optString("sender_name", "Other");
|
||||
String content = m.getString("content");
|
||||
String content = m.optString("content", "");
|
||||
String time = m.getString("send_at");
|
||||
|
||||
String label = senderId.equals(Session.currentUser.getString("internal_uuid")) ? "You" : senderName;
|
||||
@@ -1691,7 +1693,7 @@ public class ActionHandler {
|
||||
|
||||
String input = scanner.nextLine().trim();
|
||||
switch (input) {
|
||||
case "1" -> sendMessage(chatId, "private");
|
||||
case "1" -> sendMessageInteractive(chatId, "private");
|
||||
case "2" -> { viewMessagesInChat(chat); }
|
||||
case "3" -> { return false; }
|
||||
default -> System.out.println("Invalid choice.");
|
||||
@@ -1710,7 +1712,7 @@ public class ActionHandler {
|
||||
|
||||
String input = scanner.nextLine().trim();
|
||||
switch (input) {
|
||||
case "1" -> sendMessage(chatId, "private");
|
||||
case "1" -> sendMessageInteractive(chat.getId(), "private");
|
||||
case "2" -> toggleBlock(chat.getOtherUserId());
|
||||
case "3" -> { deleteChat(chatId, false); return true; }
|
||||
case "4" -> { deleteChat(chatId, true); return true; }
|
||||
@@ -1806,7 +1808,7 @@ public class ActionHandler {
|
||||
|
||||
String input = scanner.nextLine();
|
||||
switch (input) {
|
||||
case "1" -> sendMessage(chat.getId(), "group");
|
||||
case "1" -> sendMessageInteractive(chat.getId(), "group");
|
||||
case "2" -> viewGroupMembers(chat.getId());
|
||||
case "3" -> {
|
||||
if (isOwner || (isAdmin && perms.optBoolean("can_add_members", false)))
|
||||
@@ -1933,7 +1935,7 @@ public class ActionHandler {
|
||||
switch (input) {
|
||||
case "1" -> {
|
||||
if (isOwner || (isAdmin && perms.optBoolean("can_post", false))) {
|
||||
sendMessage(chat.getId(), "channel");
|
||||
sendMessageInteractive(chat.getId(), "channel");
|
||||
} else {
|
||||
System.out.println("❌ You don't have permission to post.");
|
||||
}
|
||||
@@ -3673,82 +3675,135 @@ public class ActionHandler {
|
||||
|
||||
|
||||
|
||||
public void sendMessage(UUID chatId, String receiverType) {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
// public void sendMessage(UUID chatId, String receiverType) {
|
||||
// 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();
|
||||
|
||||
System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE / AUDIO): ");
|
||||
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 / AUDIO): ");
|
||||
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();
|
||||
|
||||
// URL validation
|
||||
if (fileUrl.isEmpty()) {
|
||||
System.out.print("URL can not be empty. Try again.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fileUrl.contains(" ")) {
|
||||
System.out.println("URL cannot contain spaces. Try again.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fileUrl.isEmpty() && !fileUrl.matches("^(http|https)://.*$")) {
|
||||
System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link.");
|
||||
continue;
|
||||
}
|
||||
|
||||
System.out.print("File Type (IMAGE / VIDEO / FILE / AUDIO): ");
|
||||
String fileType = scanner.nextLine().toUpperCase();
|
||||
|
||||
Set<String> allowedFileTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE");
|
||||
while (!allowedFileTypes.contains(fileType)) {
|
||||
System.out.print("❌ Invalid type. Try again (IMAGE / VIDEO / FILE / AUDIO): ");
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshContactList() {
|
||||
// public void sendMessage(UUID chatId, String receiverType) {
|
||||
// Scanner scanner = new Scanner(System.in);
|
||||
//
|
||||
// System.out.print("Enter your message (leave empty if file only): ");
|
||||
// String content = scanner.nextLine();
|
||||
//
|
||||
// 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");
|
||||
// while (!allowedTypes.contains(messageType)) {
|
||||
// System.out.print("❌ Invalid type. Try again (TEXT / IMAGE / AUDIO / FILE / GIF): ");
|
||||
// messageType = scanner.nextLine().toUpperCase();
|
||||
// }
|
||||
//
|
||||
// JSONArray attachmentsArray = new JSONArray();
|
||||
// System.out.print("Attach files? (yes/no): ");
|
||||
// if (scanner.nextLine().equalsIgnoreCase("yes")) {
|
||||
// while (true) {
|
||||
// System.out.println("Paste the JSON you got from /upload (or leave empty to enter minimal fields):");
|
||||
// String jsonLine = scanner.nextLine().trim();
|
||||
//
|
||||
// JSONObject fileJson;
|
||||
// if (!jsonLine.isEmpty()) {
|
||||
// // انتظار خروجی کامل /upload
|
||||
// fileJson = new JSONObject(jsonLine);
|
||||
// // اگه خروجی /upload تو ریشهست، تبدیلش کن به ساختار attachment
|
||||
// fileJson = new JSONObject()
|
||||
// .put("file_url", fileJson.optString("file_url", ""))
|
||||
// .put("file_type", fileJson.optString("file_type", "FILE"))
|
||||
// .put("file_name", fileJson.optString("file_name", ""))
|
||||
// .put("file_size", fileJson.optLong("file_size", 0))
|
||||
// .put("mime_type", fileJson.optString("mime_type", ""))
|
||||
// .put("width", fileJson.isNull("width") ? JSONObject.NULL : fileJson.optInt("width"))
|
||||
// .put("height", fileJson.isNull("height") ? JSONObject.NULL : fileJson.optInt("height"))
|
||||
// .put("duration_seconds", fileJson.isNull("duration_seconds") ? JSONObject.NULL : fileJson.optInt("duration_seconds"))
|
||||
// .put("thumbnail_url", fileJson.isNull("thumbnail_url") ? JSONObject.NULL : fileJson.optString("thumbnail_url", null));
|
||||
// } else {
|
||||
// // ورودی حداقلی
|
||||
// System.out.print("File URL: ");
|
||||
// String fileUrl = scanner.nextLine();
|
||||
// System.out.print("File Type (IMAGE / AUDIO / FILE / GIF): ");
|
||||
// String fileType = scanner.nextLine().toUpperCase();
|
||||
//
|
||||
// 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); // "private"/"group"/"channel"
|
||||
// messageJson.put("receiver_id", chatId.toString()); // در private = chat_id
|
||||
// messageJson.put("content", content);
|
||||
// messageJson.put("message_type", messageType);
|
||||
// if (attachmentsArray.length() > 0) {
|
||||
// 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.optString("message","No message") : "No response"));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
private void refreshContactList() {
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("action", "get_contact_list");
|
||||
req.put("user_id", Session.currentUser.getString("user_id"));
|
||||
@@ -3860,6 +3915,18 @@ public class ActionHandler {
|
||||
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");
|
||||
if (reactions != null && !reactions.isEmpty()) {
|
||||
System.out.print(" 💬 Reactions: ");
|
||||
@@ -3894,7 +3961,7 @@ public class ActionHandler {
|
||||
}
|
||||
|
||||
if(input.equalsIgnoreCase("S")){
|
||||
sendMessage(chat.getId(), chat.getType());
|
||||
sendMessageInteractive(chat.getId(), chat.getType());
|
||||
}
|
||||
try {
|
||||
int index = Integer.parseInt(input);
|
||||
@@ -3909,6 +3976,10 @@ public class ActionHandler {
|
||||
boolean isSender = senderId.toString().equals(Session.currentUser.getString("internal_uuid"));
|
||||
boolean isChannel = chat.getType().equals("channel");
|
||||
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"));
|
||||
|
||||
@@ -3934,6 +4005,9 @@ public class ActionHandler {
|
||||
System.out.println("5. Delete");
|
||||
}
|
||||
}
|
||||
if (hasAttachments) {
|
||||
System.out.println("D. Download attachment");
|
||||
}
|
||||
|
||||
System.out.println("0. Back to message list");
|
||||
System.out.print("➤ Select an action: ");
|
||||
@@ -3966,6 +4040,15 @@ public class ActionHandler {
|
||||
System.out.println("❌ You are not allowed to delete this message.");
|
||||
}
|
||||
case "0" -> {}
|
||||
|
||||
case "D", "d" -> {
|
||||
if (hasAttachments) {
|
||||
downloadAttachmentFlow(chat, selected);
|
||||
} else {
|
||||
System.out.println("🚫 No attachments to download.");
|
||||
}
|
||||
}
|
||||
|
||||
default -> System.out.println("❌ Invalid option.");
|
||||
}
|
||||
|
||||
@@ -3976,6 +4059,122 @@ public class ActionHandler {
|
||||
}
|
||||
|
||||
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: ");
|
||||
String newContent = scanner.nextLine().trim();
|
||||
|
||||
@@ -4191,6 +4390,183 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// DownloadIndexRegistry.java
|
||||
package org.to.telegramfinalproject.Client;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class DownloadIndexRegistry {
|
||||
private static final ConcurrentHashMap<UUID, DownloadsIndex> INSTANCES = new ConcurrentHashMap<>();
|
||||
private static volatile boolean HOOK_REGISTERED = false;
|
||||
|
||||
private DownloadIndexRegistry() {}
|
||||
|
||||
public static DownloadsIndex forAccount(UUID accountId) {
|
||||
registerHookOnce();
|
||||
return INSTANCES.computeIfAbsent(accountId, DownloadsIndex::new);
|
||||
}
|
||||
|
||||
public static void closeAccount(UUID accountId) {
|
||||
DownloadsIndex idx = INSTANCES.remove(accountId);
|
||||
if (idx != null) idx.saveQuietly();
|
||||
}
|
||||
|
||||
private static synchronized void registerHookOnce() {
|
||||
if (HOOK_REGISTERED) return;
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
for (DownloadsIndex idx : INSTANCES.values()) idx.saveQuietly();
|
||||
}));
|
||||
HOOK_REGISTERED = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
|
||||
package org.to.telegramfinalproject.Client;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class DownloadsIndex {
|
||||
private final UUID accountId;
|
||||
private final Path indexFile;
|
||||
private final Map<UUID, Entry> map = new ConcurrentHashMap<>();
|
||||
|
||||
public DownloadsIndex(UUID accountId) {
|
||||
this.accountId = accountId;
|
||||
this.indexFile = resolveIndexPath(accountId.toString());
|
||||
load();
|
||||
}
|
||||
|
||||
|
||||
private static Path resolveIndexPath(String accountId) {
|
||||
String os = System.getProperty("os.name", "").toLowerCase();
|
||||
String home = System.getProperty("user.home");
|
||||
|
||||
Path dir;
|
||||
if (os.contains("win")) {
|
||||
String appData = System.getenv("APPDATA");
|
||||
dir = (appData != null)
|
||||
? Paths.get(appData, "TeleSock")
|
||||
: Paths.get(home, "AppData", "Roaming", "TeleSock");
|
||||
} else {
|
||||
dir = Paths.get(home, ".telesock");
|
||||
}
|
||||
try { Files.createDirectories(dir); } catch (IOException ignored) {}
|
||||
return dir.resolve("downloads-index-" + accountId + ".json");
|
||||
}
|
||||
|
||||
private synchronized void load() {
|
||||
map.clear();
|
||||
try {
|
||||
if (!Files.exists(indexFile)) return;
|
||||
String json = Files.readString(indexFile, StandardCharsets.UTF_8);
|
||||
if (json == null || json.isBlank()) return;
|
||||
|
||||
JSONObject root = new JSONObject(json);
|
||||
JSONObject items = root.optJSONObject("items");
|
||||
if (items == null) return;
|
||||
|
||||
for (String key : items.keySet()) {
|
||||
JSONObject e = items.getJSONObject(key);
|
||||
map.put(UUID.fromString(key), new Entry(
|
||||
e.getString("path"),
|
||||
e.optLong("size", 0L),
|
||||
e.optLong("ts", System.currentTimeMillis())
|
||||
));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("⚠️ DownloadsIndex load failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void save() throws IOException {
|
||||
JSONObject items = new JSONObject();
|
||||
for (Map.Entry<UUID, Entry> it : map.entrySet()) {
|
||||
JSONObject e = new JSONObject();
|
||||
e.put("path", it.getValue().path);
|
||||
e.put("size", it.getValue().size);
|
||||
e.put("ts", it.getValue().ts);
|
||||
items.put(it.getKey().toString(), e);
|
||||
}
|
||||
byte[] data = new JSONObject().put("items", items).toString(2).getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
Path tmp = indexFile.resolveSibling(indexFile.getFileName() + ".tmp");
|
||||
Files.write(tmp, data);
|
||||
try {
|
||||
Files.move(tmp, indexFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException ex) {
|
||||
Files.move(tmp, indexFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveQuietly() {
|
||||
try { save(); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
public Path find(UUID mediaKey) {
|
||||
Entry e = map.get(mediaKey);
|
||||
if (e == null) return null;
|
||||
Path p = Paths.get(e.path);
|
||||
if (Files.exists(p)) return p;
|
||||
map.remove(mediaKey);
|
||||
saveQuietly();
|
||||
return null;
|
||||
}
|
||||
|
||||
public void put(UUID mediaKey, Path path, long size) {
|
||||
map.put(mediaKey, new Entry(path.toString(), size, System.currentTimeMillis()));
|
||||
saveQuietly();
|
||||
}
|
||||
|
||||
public void remove(UUID mediaKey) {
|
||||
map.remove(mediaKey);
|
||||
saveQuietly();
|
||||
}
|
||||
|
||||
private static final class Entry {
|
||||
final String path; final long size; final long ts;
|
||||
Entry(String path, long size, long ts) {
|
||||
this.path = path; this.size = size; this.ts = ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import org.to.telegramfinalproject.UI.MainController;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -15,6 +18,7 @@ import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class IncomingMessageListener implements Runnable {
|
||||
private final BufferedReader in;
|
||||
private volatile boolean running = true;
|
||||
public enum UIMode { CONSOLE, UI }
|
||||
private final UIMode uiMode; // runtime mode
|
||||
|
||||
@@ -36,16 +40,47 @@ public class IncomingMessageListener implements Runnable {
|
||||
try {
|
||||
System.out.println("👂 Real-Time Listener started.");
|
||||
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
while (running) {
|
||||
if (TelegramClient.mediaBusy.get()) {
|
||||
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);
|
||||
|
||||
//if it has reqID answer
|
||||
// --- Media ACK routing by message_id ---
|
||||
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")) {
|
||||
String requestId = response.getString("request_id");
|
||||
String requestId = response.optString("request_id", "");
|
||||
System.out.println("📬 Response with request_id: " + requestId);
|
||||
System.out.println("📬 Full response: " + response.toString(2));
|
||||
|
||||
@@ -56,15 +91,12 @@ public class IncomingMessageListener implements Runnable {
|
||||
System.out.println("⚠️ No pending queue for request_id = " + requestId + ". Putting in responseQueue...");
|
||||
TelegramClient.responseQueue.put(response);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//if it has action check it
|
||||
// --- Real-time actions ---
|
||||
if (response.has("action")) {
|
||||
String action = response.getString("action");
|
||||
String action = response.optString("action", "");
|
||||
System.out.println("🎯 [Listener] Action received: " + response.toString(2));
|
||||
System.out.println("🎯 Received action: " + action);
|
||||
|
||||
@@ -73,11 +105,12 @@ public class IncomingMessageListener implements Runnable {
|
||||
} else {
|
||||
TelegramClient.responseQueue.put(response);
|
||||
}
|
||||
|
||||
} else if (response.has("status") && response.has("message")) {
|
||||
TelegramClient.responseQueue.put(response); // general answer
|
||||
// General success/error
|
||||
TelegramClient.responseQueue.put(response);
|
||||
} else {
|
||||
TelegramClient.responseQueue.put(response); // fallback
|
||||
// Fallback
|
||||
TelegramClient.responseQueue.put(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +127,9 @@ public class IncomingMessageListener implements Runnable {
|
||||
"update_group_or_channel", "chat_deleted",
|
||||
"blocked_by_user", "unblocked_by_user", "message_seen",
|
||||
"removed_from_group", "removed_from_channel",
|
||||
"became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated","created_private_chat" , "message_reacted" , "message_unreacted","chat_updated" -> true;
|
||||
"became_admin", "removed_admin", "ownership_transferred",
|
||||
"admin_permissions_updated", "created_private_chat",
|
||||
"message_reacted", "message_unreacted" , "chat_updated"-> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
@@ -113,9 +148,11 @@ public class IncomingMessageListener implements Runnable {
|
||||
System.out.println("🔄 Chat list changed. Updating...");
|
||||
Session.forceRefreshChatList = true;
|
||||
|
||||
String chatId = msg.getString("chat_id");
|
||||
String chatType = msg.getString("chat_type");
|
||||
ActionHandler.requestChatInfo(chatId, chatType);
|
||||
String chatId = msg.optString("chat_id", "");
|
||||
String chatType = msg.optString("chat_type", "");
|
||||
if (!chatId.isBlank() && !chatType.isBlank()) {
|
||||
ActionHandler.requestChatInfo(chatId, chatType);
|
||||
}
|
||||
|
||||
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...");
|
||||
@@ -198,78 +235,136 @@ public class IncomingMessageListener implements Runnable {
|
||||
System.out.println("\n❓ Unknown real-time action: " + action);
|
||||
System.out.println(msg.toString(2));
|
||||
}
|
||||
default -> displayRealTimeMessage(action, msg);
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
UUID chatUUID = UUID.fromString(msg.getString("chat_id"));
|
||||
String newTime = msg.optString("last_message_time", null);
|
||||
if (newTime == null || newTime.isBlank()) return;
|
||||
|
||||
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()); // 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
|
||||
});
|
||||
|
||||
|
||||
updateOneList(Session.chatList, chatUUID, newTime);
|
||||
updateOneList(Session.activeChats, chatUUID, newTime);
|
||||
updateOneList(Session.archivedChats, chatUUID, newTime);
|
||||
|
||||
sortByLastMessageTime(Session.chatList);
|
||||
sortByLastMessageTime(Session.activeChats);
|
||||
sortByLastMessageTime(Session.archivedChats);
|
||||
|
||||
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 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 {
|
||||
String chatType = data.getString("chat_type");
|
||||
String chatId = data.optString("group_id", data.optString("channel_id", data.optString("chat_id", null)));
|
||||
String chatType = data.optString("chat_type", "");
|
||||
String chatId = data.optString("group_id",
|
||||
data.optString("channel_id", data.optString("chat_id", "")));
|
||||
|
||||
if (chatId == null) {
|
||||
if (chatId.isBlank()) {
|
||||
System.out.println("⚠️ No valid ID found in real-time data: " + data.toString(2));
|
||||
return;
|
||||
}
|
||||
@@ -277,11 +372,10 @@ public class IncomingMessageListener implements Runnable {
|
||||
System.out.println("\n🔄 Your admin status changed. Updating chat info...");
|
||||
|
||||
try {
|
||||
// 1. get chat info
|
||||
JSONObject chatInfoReq = new JSONObject();
|
||||
chatInfoReq.put("action", "get_chat_info");
|
||||
chatInfoReq.put("receiver_id", chatId);
|
||||
chatInfoReq.put("receiver_type", chatType);
|
||||
JSONObject chatInfoReq = new JSONObject()
|
||||
.put("action", "get_chat_info")
|
||||
.put("receiver_id", chatId)
|
||||
.put("receiver_type", chatType);
|
||||
System.out.println("📤 Sending get_chat_info: " + chatInfoReq);
|
||||
JSONObject chatInfoResp = ActionHandler.sendWithResponse(chatInfoReq);
|
||||
JSONObject chatData = chatInfoResp.getJSONObject("data");
|
||||
@@ -307,21 +401,17 @@ public class IncomingMessageListener implements Runnable {
|
||||
Session.currentChatEntry = chat;
|
||||
});
|
||||
|
||||
// 2. get permission
|
||||
JSONObject permissionReq = new JSONObject();
|
||||
if (chatType.equalsIgnoreCase("group")) {
|
||||
permissionReq.put("action", "get_group_permissions");
|
||||
permissionReq.put("group_id", chatId);
|
||||
permissionReq.put("action", "get_group_permissions").put("group_id", chatId);
|
||||
} else {
|
||||
permissionReq.put("action", "get_channel_permissions");
|
||||
permissionReq.put("channel_id", chatId);
|
||||
permissionReq.put("action", "get_channel_permissions").put("channel_id", chatId);
|
||||
}
|
||||
|
||||
JSONObject permissionResp = ActionHandler.sendWithResponse(permissionReq);
|
||||
JSONObject perm = permissionResp.getJSONObject("data");
|
||||
entry.ifPresent(chat -> chat.setPermissions(perm));
|
||||
|
||||
// 3. set currentChatId
|
||||
Session.currentChatId = chatUUID.toString();
|
||||
|
||||
System.out.println("🧪 Checking refresh conditions...");
|
||||
@@ -344,20 +434,11 @@ public class IncomingMessageListener implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void displayRealTimeMessage(String action, JSONObject msg) {
|
||||
switch (action) {
|
||||
case "new_message" -> {
|
||||
String senderName = msg.optString("sender_name","Unknown");
|
||||
String content = msg.optString("content","(empty)");
|
||||
String content = msg.optString("content","");
|
||||
String sendAt = msg.optString("send_at","-");
|
||||
String chatId = msg.optString("receiver_id", msg.optString("chat_id",""));
|
||||
String kind = msg.optString("kind","plain");
|
||||
@@ -376,47 +457,47 @@ public class IncomingMessageListener implements Runnable {
|
||||
Session.currentChatId != null && Session.currentChatId.equals(chatId);
|
||||
|
||||
if (isInCurrentChat) {
|
||||
if (content.isBlank()) content = "(no content)"; // برای مدیا بدون کپشن
|
||||
System.out.println(senderName + ": " + prefix + content + " (" + sendAt + ")");
|
||||
} else {
|
||||
System.out.println("💬 Message from " + senderName + ": " + prefix + content);
|
||||
String preview = content.isBlank() ? "[media]" : content;
|
||||
System.out.println("💬 Message from " + senderName + ": " + prefix + preview);
|
||||
Session.forceRefreshChatList = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case "message_edited" -> {
|
||||
System.out.println("\n✏️ Message Edited:");
|
||||
System.out.println("ID: " + msg.getString("message_id"));
|
||||
System.out.println("New Content: " + msg.getString("new_content"));
|
||||
System.out.println("Edit Time: " + msg.getString("edited_at"));
|
||||
System.out.println("ID: " + msg.optString("message_id",""));
|
||||
System.out.println("New Content: " + msg.optString("new_content",""));
|
||||
System.out.println("Edit Time: " + msg.optString("edited_at",""));
|
||||
}
|
||||
case "message_deleted_global" -> {
|
||||
System.out.println("\n🗑️ Message Deleted:");
|
||||
System.out.println("Message ID: " + msg.getString("message_id"));
|
||||
System.out.println("Message ID: " + msg.optString("message_id",""));
|
||||
}
|
||||
case "message_reacted", "message_unreacted" -> {
|
||||
String mid = msg.getString("message_id");
|
||||
String emoji = msg.getString("emoji");
|
||||
JSONObject counts = msg.optJSONObject("counts");
|
||||
String mid = msg.optString("message_id","");
|
||||
String emoji = msg.optString("emoji","");
|
||||
int n = msg.optInt("count_for_emoji", 0);
|
||||
System.out.println("\n⭐ Reaction update on " + mid + " : " + emoji + " → " + n);
|
||||
}
|
||||
|
||||
case "user_status_changed" -> {
|
||||
System.out.println("\n🔄 User Status Changed:");
|
||||
System.out.println("User: " + msg.getString("user_id"));
|
||||
System.out.println("Status: " + msg.getString("status"));
|
||||
System.out.println("User: " + msg.optString("user_id",""));
|
||||
System.out.println("Status: " + msg.optString("status",""));
|
||||
}
|
||||
case "blocked_by_user" -> {
|
||||
System.out.println("\n⛔ You were blocked by user: " + msg.getString("blocker_id"));
|
||||
System.out.println("\n⛔ You were blocked by user: " + msg.optString("blocker_id",""));
|
||||
}
|
||||
case "unblocked_by_user" -> {
|
||||
System.out.println("\n✅ You were unblocked by user: " + msg.getString("unblocker_id"));
|
||||
System.out.println("\n✅ You were unblocked by user: " + msg.optString("unblocker_id",""));
|
||||
}
|
||||
case "message_seen" -> {
|
||||
System.out.println("\n👁️ Your message was seen:");
|
||||
System.out.println("Message ID: " + msg.getString("message_id"));
|
||||
System.out.println("Seen at: " + msg.getString("seen_at"));
|
||||
System.out.println("Message ID: " + msg.optString("message_id",""));
|
||||
System.out.println("Seen at: " + msg.optString("seen_at",""));
|
||||
}
|
||||
default -> {
|
||||
System.out.println("\n❓ Unknown real-time action: " + action);
|
||||
@@ -427,6 +508,9 @@ public class IncomingMessageListener implements Runnable {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static LocalDateTime parseIsoFlexible(String iso) {
|
||||
if (iso == null || iso.isBlank()) return null;
|
||||
try { return LocalDateTime.parse(iso); } catch (Exception ignore) {}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.to.telegramfinalproject.Client;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
|
||||
|
||||
class MediaSender {
|
||||
|
||||
public static void sendImageOrAudio(Socket socket,
|
||||
UUID senderId,
|
||||
String receiverType,
|
||||
UUID receiverId,
|
||||
File file,
|
||||
String messageType, // "IMAGE" یا "AUDIO"
|
||||
String captionOrEmpty) throws Exception {
|
||||
|
||||
if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType))
|
||||
throw new IllegalArgumentException("Only IMAGE/AUDIO");
|
||||
|
||||
// 1) اعلام سوییچ به باینری
|
||||
PrintWriter textOut = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
|
||||
textOut.println("MEDIA");
|
||||
|
||||
// 2) متادیتا
|
||||
String mime = Files.probeContentType(file.toPath());
|
||||
if (mime == null) mime = "application/octet-stream";
|
||||
|
||||
Integer width = null, height = null;
|
||||
if ("IMAGE".equals(messageType)) {
|
||||
try {
|
||||
BufferedImage img = ImageIO.read(file);
|
||||
if (img != null) { width = img.getWidth(); height = img.getHeight(); }
|
||||
} catch (Exception ignore) {}
|
||||
}
|
||||
|
||||
UUID messageId = UUID.randomUUID();
|
||||
JSONObject header = new JSONObject()
|
||||
.put("message_id", messageId.toString())
|
||||
.put("sender_id", senderId.toString())
|
||||
.put("receiver_type", receiverType)
|
||||
.put("receiver_id", receiverId.toString())
|
||||
.put("message_type", messageType)
|
||||
.put("file_name", file.getName())
|
||||
.put("mime_type", mime)
|
||||
.put("file_size", file.length())
|
||||
.put("text", captionOrEmpty == null ? "" : captionOrEmpty);
|
||||
|
||||
if (width != null) header.put("width", width);
|
||||
if (height != null) header.put("height", height);
|
||||
|
||||
byte[] headerBytes = header.toString().getBytes("UTF-8");
|
||||
|
||||
// 3) ارسال فریم باینری
|
||||
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
|
||||
dos.writeInt(0x4D444D31); // MAGIC
|
||||
dos.writeInt(headerBytes.length); // headerLen
|
||||
dos.write(headerBytes); // header
|
||||
dos.writeLong(file.length()); // contentLen
|
||||
|
||||
try (InputStream fis = new BufferedInputStream(new FileInputStream(file))) {
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = fis.read(buf)) != -1) {
|
||||
dos.write(buf, 0, n);
|
||||
}
|
||||
}
|
||||
dos.flush();
|
||||
|
||||
// (اختیاری) Ack متنی
|
||||
BufferedReader textIn = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
|
||||
String ack = textIn.readLine();
|
||||
if (!"OK".equalsIgnoreCase(ack)) {
|
||||
throw new IOException("Server did not ACK: " + ack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public class Session {
|
||||
public static ChatEntry currentChatEntry = null;
|
||||
public static List<ContactEntry> contactEntries = new ArrayList<>();
|
||||
public static boolean inContactListMenu = false;
|
||||
|
||||
public static DownloadsIndex downloadsIndex = null;
|
||||
|
||||
public static String getUserUUID() {
|
||||
if (currentUser.has("uuid")) return currentUser.getString("uuid");
|
||||
@@ -110,4 +110,7 @@ public class Session {
|
||||
.filter(ChatEntry::isArchived)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void setDownloadIndex(DownloadsIndex idx){ this.downloadsIndex = idx; }
|
||||
|
||||
}
|
||||
@@ -255,234 +255,7 @@ public class SidebarHandler {
|
||||
actionHandler.showContactList();
|
||||
}
|
||||
|
||||
// public void getSavedMessagesData(String userId) {
|
||||
// try {
|
||||
// // Step 1: Create the request
|
||||
// JSONObject request = new JSONObject();
|
||||
// request.put("action", "get_saved_messages");
|
||||
// request.put("user_id", userId);
|
||||
//
|
||||
// // Step 2: Send request and wait for response
|
||||
// JSONObject response = ActionHandler.sendWithResponse(request);
|
||||
//
|
||||
// // Step 3: Check the response
|
||||
// if (!response.optString("status", "fail").equals("success")) {
|
||||
// System.out.println("Failed to open Saved Messages chat: " + response.optString("message", "Unknown error"));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Step 4: Extract "data" object
|
||||
// JSONObject data = response.getJSONObject("data");
|
||||
// UUID chatId = UUID.fromString(data.getString("chat_id"));
|
||||
// JSONArray messagesArray = data.getJSONArray("messages");
|
||||
//
|
||||
// // Step 5: Parse messages
|
||||
// List<Message> messages = new ArrayList<>();
|
||||
// if (!messagesArray.isEmpty()) {
|
||||
// for (int i = 0; i < messagesArray.length(); i++) {
|
||||
// JSONObject msgJson = messagesArray.getJSONObject(i);
|
||||
//
|
||||
// // Safely extract optional UUIDs
|
||||
// UUID replyToId = null;
|
||||
// String replyToIdStr = msgJson.optString("reply_to_id", null);
|
||||
// if (replyToIdStr != null && !replyToIdStr.equals("null")) {
|
||||
// replyToId = UUID.fromString(replyToIdStr);
|
||||
// }
|
||||
//
|
||||
// UUID originalMessageId = null;
|
||||
// String originalMessageIdStr = msgJson.optString("original_message_id", null);
|
||||
// if (originalMessageIdStr != null && !originalMessageIdStr.equals("null")) {
|
||||
// originalMessageId = UUID.fromString(originalMessageIdStr);
|
||||
// }
|
||||
//
|
||||
// UUID forwardedBy = null;
|
||||
// String forwardedByStr = msgJson.optString("forwarded_by", null);
|
||||
// if (forwardedByStr != null && !forwardedByStr.equals("null")) {
|
||||
// forwardedBy = UUID.fromString(forwardedByStr);
|
||||
// }
|
||||
//
|
||||
// UUID forwardedFrom = null;
|
||||
// String forwardedFromStr = msgJson.optString("forwarded_from", null);
|
||||
// if (forwardedFromStr != null && !forwardedFromStr.equals("null")) {
|
||||
// forwardedFrom = UUID.fromString(forwardedFromStr);
|
||||
// }
|
||||
//
|
||||
// Message msg = new Message(
|
||||
// UUID.fromString(msgJson.getString("message_id")),
|
||||
// UUID.fromString(msgJson.getString("sender_id")),
|
||||
// msgJson.getString("receiver_type"),
|
||||
// UUID.fromString(msgJson.getString("receiver_id")),
|
||||
// msgJson.getString("content"),
|
||||
// msgJson.getString("message_type"),
|
||||
// LocalDateTime.parse(msgJson.getString("send_at").replace(" ", "T")),
|
||||
// msgJson.getString("status"),
|
||||
// replyToId,
|
||||
// msgJson.getBoolean("is_edited"),
|
||||
// originalMessageId,
|
||||
// forwardedBy,
|
||||
// forwardedFrom,
|
||||
// msgJson.getBoolean("is_deleted_globally"),
|
||||
// LocalDateTime.parse(msgJson.getString("edited_at").replace(" ", "T"))
|
||||
// );
|
||||
//
|
||||
// messages.add(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Step 6: Add to active chats if not already present
|
||||
// boolean alreadyExists = Session.activeChats.stream()
|
||||
// .anyMatch(entry -> entry.getId().equals(chatId));
|
||||
// if (!alreadyExists) {
|
||||
// ChatEntry savedEntry = new ChatEntry(
|
||||
// chatId,
|
||||
// "Saved-Messages",
|
||||
// "Saved Messages",
|
||||
// "📌", // or use a URL string if you have an icon for saved messages
|
||||
// "private",
|
||||
// messages.isEmpty() ? null : messages.get(messages.size() - 1).getSend_at()
|
||||
// );
|
||||
// savedEntry.setSavedMessages(true);
|
||||
// Session.activeChats.add(savedEntry);
|
||||
// }
|
||||
//
|
||||
// // Step 7: Show chat
|
||||
// showSavedMessages(chatId, messages);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// System.out.println("An error occurred while retrieving Saved Messages.");
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
|
||||
// private void showSavedMessages(UUID chatId, List<Message> messages) {
|
||||
// Scanner scanner = new Scanner(System.in);
|
||||
//
|
||||
// System.out.println("==== Saved Messages ====");
|
||||
// if (messages == null || messages.isEmpty()) {
|
||||
// System.out.println("No messages yet.");
|
||||
// } else {
|
||||
// for (Message msg : messages) {
|
||||
// System.out.println("[" + msg.getSend_at() + "] " + msg.getContent());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// System.out.println("\n(Type your message below, or type 0 to exit)");
|
||||
//
|
||||
// while (true) {
|
||||
// System.out.print("You: ");
|
||||
// String content = scanner.nextLine().trim();
|
||||
// if (content.equals("0")) {
|
||||
// System.out.println("Exiting Saved Messages.");
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// System.out.print("Enter message type (TEXT / IMAGE / VIDEO / FILE / AUDIO): ");
|
||||
// String messageType = scanner.nextLine().trim().toUpperCase();
|
||||
// Set<String> allowedTypes = Set.of("TEXT", "IMAGE", "VIDEO", "FILE", "AUDIO");
|
||||
// while (!allowedTypes.contains(messageType)) {
|
||||
// System.out.print("Invalid type. Try again (TEXT / IMAGE / VIDEO / FILE / AUDIO): ");
|
||||
// messageType = scanner.nextLine().trim().toUpperCase();
|
||||
// }
|
||||
//
|
||||
// // Attachments (اختیاری)
|
||||
// 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().trim();
|
||||
//
|
||||
// if (fileUrl.isEmpty()) {
|
||||
// System.out.println("URL can not be empty. Try again.");
|
||||
// continue;
|
||||
// }
|
||||
// if (fileUrl.contains(" ")) {
|
||||
// System.out.println("URL cannot contain spaces. Try again.");
|
||||
// continue;
|
||||
// }
|
||||
// if (!fileUrl.matches("^(http|https)://.*$")) {
|
||||
// System.out.println("Invalid URL format. Please enter a valid HTTP/HTTPS link.");
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// System.out.print("File Type (IMAGE / VIDEO / FILE / AUDIO): ");
|
||||
// String fileType = scanner.nextLine().trim().toUpperCase();
|
||||
// Set<String> allowedFileTypes = Set.of("IMAGE", "VIDEO", "FILE", "AUDIO");
|
||||
// while (!allowedFileTypes.contains(fileType)) {
|
||||
// System.out.print("Invalid type. Try again (IMAGE / VIDEO / FILE / AUDIO): ");
|
||||
// fileType = scanner.nextLine().trim().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;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // درخواست مطابق هندلر send_message
|
||||
// JSONObject request = new JSONObject();
|
||||
// request.put("action", "send_message");
|
||||
// request.put("receiver_type", "private");
|
||||
// request.put("receiver_id", chatId.toString()); // chat_id
|
||||
// request.put("content", content);
|
||||
// request.put("message_type", messageType);
|
||||
// if (attachmentsArray.length() > 0) {
|
||||
// request.put("attachments", attachmentsArray);
|
||||
// }
|
||||
//
|
||||
// JSONObject response = ActionHandler.sendWithResponse(request);
|
||||
// if (!"success".equalsIgnoreCase(response.optString("status"))) {
|
||||
// System.out.println("Failed to send message: " + response.optString("message", "Unknown error"));
|
||||
// } else {
|
||||
// System.out.println("Message sent.");
|
||||
//
|
||||
// Message justSent = new Message(
|
||||
// UUID.fromString(response.getJSONObject("data").getString("message_id")),
|
||||
// /* senderId */ userUUID,
|
||||
// /* receiverId */ chatId,
|
||||
// /* type */ "private",
|
||||
// /* content */ content,
|
||||
// /* msgType */ messageType,
|
||||
// /* send_at */ java.time.LocalDateTime.now()
|
||||
// );
|
||||
// messages.add(justSent);
|
||||
// System.out.println("[" + justSent.getSend_at() + "] " + justSent.getContent());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// public void openSavedMessages() {
|
||||
// JSONObject req = new JSONObject().put("action", "get_or_create_saved_messages");
|
||||
// JSONObject res = sendWithResponse(req);
|
||||
// if (res == null || !"success".equals(res.optString("status"))) {
|
||||
// System.out.println("❌ Could not open Saved Messages: " + res.optString("message",""));
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// ActionHandler.requestChatList();
|
||||
// String chatId = res.getJSONObject("data").getString("chat_id");
|
||||
//
|
||||
// JSONObject mreq = new JSONObject()
|
||||
// .put("action", "get_messages")
|
||||
// .put("receiver_type", "private")
|
||||
// .put("receiver_id", chatId)
|
||||
// .put("offset", 0)
|
||||
// .put("limit", 50);
|
||||
//
|
||||
// JSONObject mres = sendWithResponse(mreq);
|
||||
// JSONArray msgs = (mres != null && mres.has("data"))
|
||||
// ? mres.getJSONObject("data").optJSONArray("messages")
|
||||
// : new JSONArray();
|
||||
//
|
||||
// List<Message> messages = parseMessages(msgs); // تبدیل JSON → Message
|
||||
// showSavedMessages(UUID.fromString(chatId), messages);
|
||||
// }
|
||||
//
|
||||
|
||||
|
||||
|
||||
@@ -774,7 +547,6 @@ public class SidebarHandler {
|
||||
}
|
||||
|
||||
private String padBoxLine(String text, int width) {
|
||||
// عرض: width، دو طرف │ │
|
||||
final int inner = width - 2;
|
||||
if (text.length() > inner) {
|
||||
text = text.substring(0, inner - 1) + "…";
|
||||
@@ -804,58 +576,5 @@ public class SidebarHandler {
|
||||
return approved ;
|
||||
}
|
||||
|
||||
private List<Message> parseMessages(JSONArray msgs) {
|
||||
List<Message> list = new ArrayList<>();
|
||||
if (msgs == null) return list;
|
||||
|
||||
for (int i = 0; i < msgs.length(); i++) {
|
||||
try {
|
||||
JSONObject obj = msgs.getJSONObject(i);
|
||||
|
||||
UUID messageId = UUID.fromString(obj.getString("message_id"));
|
||||
UUID senderId = UUID.fromString(obj.getString("sender_id"));
|
||||
String receiverType= obj.getString("receiver_type"); // "private" | "group" | "channel"
|
||||
UUID receiverId = UUID.fromString(obj.getString("receiver_id")); // برای private = chat_id
|
||||
String content = obj.optString("content", "");
|
||||
String messageType = obj.optString("message_type", "TEXT");
|
||||
LocalDateTime sent = LocalDateTime.parse(obj.getString("send_at"));
|
||||
|
||||
Message m = new Message(
|
||||
messageId, senderId, receiverId, receiverType, content, messageType, sent
|
||||
);
|
||||
|
||||
if (obj.has("status") && !obj.isNull("status")) {
|
||||
try { m.setStatus(obj.getString("status")); } catch (Exception ignore) {}
|
||||
}
|
||||
if (obj.has("reply_to_id") && !obj.isNull("reply_to_id")) {
|
||||
try { m.setReply_to_id(UUID.fromString(obj.getString("reply_to_id"))); } catch (Exception ignore) {}
|
||||
}
|
||||
if (obj.has("forwarded_by") && !obj.isNull("forwarded_by")) {
|
||||
try { m.setForwarded_by(UUID.fromString(obj.getString("forwarded_by"))); } catch (Exception ignore) {}
|
||||
}
|
||||
if (obj.has("forwarded_from") && !obj.isNull("forwarded_from")) {
|
||||
try { m.setForwarded_from(UUID.fromString(obj.getString("forwarded_from"))); } catch (Exception ignore) {}
|
||||
}
|
||||
|
||||
// // ضمیمهها (اگر در مدل Message متد addAttachment داری)
|
||||
// if (obj.has("attachments") && !obj.isNull("attachments")) {
|
||||
// try {
|
||||
// JSONArray atts = obj.getJSONArray("attachments");
|
||||
// for (int j = 0; j < atts.length(); j++) {
|
||||
// JSONObject a = atts.getJSONObject(j);
|
||||
// String fileUrl = a.getString("file_url");
|
||||
// String fileType = a.getString("file_type");
|
||||
// FileAttachment fa = new FileAttachment(fileUrl, fileType);
|
||||
// try { m.addAttachment(fa); } catch (Exception ignore) {}
|
||||
// }
|
||||
// } catch (Exception ignore) {}
|
||||
// }
|
||||
|
||||
list.add(m);
|
||||
} catch (Exception perItem) {
|
||||
perItem.printStackTrace();
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.to.telegramfinalproject.Client;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public final class SocketMediaDownloader {
|
||||
private static final int MAGIC_DL = 0x4D444D32;
|
||||
private final PrintWriter outText; // NEW
|
||||
private final DataInputStream inBin;
|
||||
private final DataOutputStream outBin;
|
||||
|
||||
public SocketMediaDownloader(PrintWriter outText, DataInputStream inBin, DataOutputStream outBin) {
|
||||
this.outText = outText;
|
||||
this.inBin = inBin;
|
||||
this.outBin = outBin;
|
||||
}
|
||||
|
||||
|
||||
public java.nio.file.Path download(java.util.UUID mediaKey, java.nio.file.Path saveDir, String fileNameHint) throws Exception {
|
||||
outText.print("MEDIA_DL\n");
|
||||
outText.flush();
|
||||
|
||||
org.json.JSONObject req = new org.json.JSONObject()
|
||||
.put("op","download")
|
||||
.put("media_key", mediaKey.toString())
|
||||
.put("offset", 0);
|
||||
byte[] hb = req.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
outBin.writeInt(MAGIC_DL);
|
||||
outBin.writeInt(hb.length);
|
||||
outBin.write(hb);
|
||||
outBin.flush();
|
||||
|
||||
int magic = inBin.readInt();
|
||||
if (magic != MAGIC_DL) throw new java.io.IOException("bad magic");
|
||||
|
||||
int hlen = inBin.readInt();
|
||||
byte[] hbytes = inBin.readNBytes(hlen);
|
||||
org.json.JSONObject hdr = new org.json.JSONObject(new String(hbytes, java.nio.charset.StandardCharsets.UTF_8));
|
||||
if (!"success".equalsIgnoreCase(hdr.optString("status"))) {
|
||||
throw new java.io.IOException("download error: " + hdr.optString("message"));
|
||||
}
|
||||
|
||||
long contentLen = inBin.readLong();
|
||||
String serverName = hdr.optString("file_name", fileNameHint != null ? fileNameHint : mediaKey.toString());
|
||||
|
||||
java.nio.file.Files.createDirectories(saveDir);
|
||||
java.nio.file.Path dest = uniquePath(saveDir, serverName);
|
||||
|
||||
try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(dest)) {
|
||||
byte[] buf = new byte[8192];
|
||||
long remain = contentLen;
|
||||
while (remain > 0) {
|
||||
int toRead = (int) Math.min(buf.length, remain);
|
||||
int n = inBin.read(buf, 0, toRead);
|
||||
if (n == -1) throw new java.io.EOFException("unexpected EOF");
|
||||
os.write(buf, 0, n);
|
||||
remain -= n;
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
private static java.nio.file.Path uniquePath(java.nio.file.Path dir, String name) throws java.io.IOException {
|
||||
java.nio.file.Path p = dir.resolve(name);
|
||||
if (!java.nio.file.Files.exists(p)) return p;
|
||||
String base = name, ext = "";
|
||||
int dot = name.lastIndexOf('.');
|
||||
if (dot >= 0) { base = name.substring(0, dot); ext = name.substring(dot); }
|
||||
int i = 1;
|
||||
while (java.nio.file.Files.exists(dir.resolve(base + " (" + i + ")" + ext))) i++;
|
||||
return dir.resolve(base + " (" + i + ")" + ext);
|
||||
}
|
||||
}
|
||||
@@ -128,10 +128,7 @@ package org.to.telegramfinalproject.Client;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
@@ -140,9 +137,11 @@ import java.util.UUID;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class TelegramClient {
|
||||
private static final String SERVER_HOST = "localhost";
|
||||
private static final int SERVER_PORT = 8080;
|
||||
private static final int SERVER_PORT = 8000;
|
||||
|
||||
private static TelegramClient instance;
|
||||
@@ -158,6 +157,13 @@ public class TelegramClient {
|
||||
public static final BlockingQueue<JSONObject> responseQueue = new LinkedBlockingQueue<>();
|
||||
public static final Map<String, BlockingQueue<JSONObject>> pendingResponses = new ConcurrentHashMap<>();
|
||||
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;
|
||||
|
||||
@@ -168,8 +174,14 @@ public class TelegramClient {
|
||||
|
||||
public static synchronized TelegramClient getInstance() {
|
||||
if (instance == null) instance = new TelegramClient();
|
||||
public static SocketMediaDownloader getDownloader() {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
public static TelegramClient getInstance() {
|
||||
return instance;
|
||||
}
|
||||
private DataOutputStream outBin;
|
||||
|
||||
// public void startConsole() {
|
||||
// try {
|
||||
@@ -184,6 +196,18 @@ public class TelegramClient {
|
||||
|
||||
public void startConsole() {
|
||||
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();
|
||||
initHandlerIfNeeded();
|
||||
startListenerOnce(IncomingMessageListener.UIMode.CONSOLE); // ← کنسول
|
||||
@@ -269,7 +293,7 @@ public class TelegramClient {
|
||||
|
||||
UUID internalId = UUID.fromString(Session.currentUser.getString("internal_uuid"));
|
||||
loggedInUserId = internalId;
|
||||
|
||||
this.downloadIndex = DownloadIndexRegistry.forAccount(internalId);
|
||||
handler.userMenu(internalId);
|
||||
} else {
|
||||
System.out.println("❌ Login failed.");
|
||||
|
||||
@@ -195,7 +195,7 @@ public class ChannelDatabase {
|
||||
""";
|
||||
|
||||
try (Connection conn = ConnectionDb.connect()) {
|
||||
// مرحله اول: ساخت کانال
|
||||
|
||||
PreparedStatement stmt = conn.prepareStatement(sql);
|
||||
stmt.setString(1, channel.getChannel_id());
|
||||
stmt.setString(2, channel.getChannel_name());
|
||||
@@ -208,9 +208,8 @@ public class ChannelDatabase {
|
||||
if (!rs.next()) return false;
|
||||
|
||||
UUID internalUUID = (UUID) rs.getObject("internal_uuid");
|
||||
channel.setInternal_uuid(internalUUID); // اختیاری برای پیگیری بعدی
|
||||
channel.setInternal_uuid(internalUUID);
|
||||
|
||||
// مرحله دوم: افزودن کاربر به لیست سابسکرایبرها
|
||||
PreparedStatement subStmt = conn.prepareStatement(subscriberSql);
|
||||
subStmt.setObject(1, internalUUID);
|
||||
subStmt.setObject(2, creatorId);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.to.telegramfinalproject.Database;
|
||||
|
||||
import org.to.telegramfinalproject.Models.FileAttachment;
|
||||
import org.to.telegramfinalproject.Models.MediaRow;
|
||||
import org.to.telegramfinalproject.Models.Message;
|
||||
import org.to.telegramfinalproject.Utils.ChannelPermissionUtil;
|
||||
|
||||
import java.sql.*;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -66,6 +68,134 @@ 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) {
|
||||
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)) {
|
||||
@@ -554,30 +684,34 @@ public class MessageDatabase {
|
||||
|
||||
public static List<FileAttachment> getAttachments(UUID messageId) {
|
||||
List<FileAttachment> attachments = new ArrayList<>();
|
||||
String sql = "SELECT file_url, file_type FROM message_attachments WHERE message_id = ?";
|
||||
|
||||
String sql = "SELECT file_url, file_type, file_name, file_size, mime_type, width, height, duration_seconds, thumbnail_url " +
|
||||
"FROM message_attachments WHERE message_id = ? ORDER BY uploaded_at";
|
||||
try (Connection conn = ConnectionDb.connect();
|
||||
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||
|
||||
stmt.setObject(1, messageId);
|
||||
ResultSet rs = stmt.executeQuery();
|
||||
|
||||
while (rs.next()) {
|
||||
attachments.add(new FileAttachment(
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static LocalDateTime getLastMessageTimeBetween(UUID user1, UUID user2, String type) {
|
||||
String sql = """
|
||||
SELECT MAX(send_at) FROM messages
|
||||
@@ -1163,6 +1297,173 @@ 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) {
|
||||
final String sql =
|
||||
"SELECT * FROM messages " +
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.to.telegramfinalproject.Database;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.to.telegramfinalproject.Models.MediaRow;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
@@ -8,6 +9,7 @@ import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MessageReactionDatabase {
|
||||
@@ -73,4 +75,8 @@ public class MessageReactionDatabase {
|
||||
return counts; // مثال: {"❤️":2,"👍":1}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -388,5 +388,26 @@ public class PrivateChatDatabase {
|
||||
rs.getBoolean("user2_deleted")
|
||||
);
|
||||
}
|
||||
public static boolean isParticipant(java.util.UUID chatId, java.util.UUID userId) {
|
||||
String sql = """
|
||||
SELECT 1
|
||||
FROM private_chat
|
||||
WHERE chat_id = ?
|
||||
AND (user1_id = ? OR user2_id = ?)
|
||||
LIMIT 1
|
||||
""";
|
||||
try (var c = ConnectionDb.connect();
|
||||
var ps = c.prepareStatement(sql)) {
|
||||
ps.setObject(1, chatId);
|
||||
ps.setObject(2, userId);
|
||||
ps.setObject(3, userId);
|
||||
try (var rs = ps.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class Contact {
|
||||
this.added_at = LocalDateTime.now();
|
||||
}
|
||||
|
||||
|
||||
public void setUser_id(UUID user_id){this.user_id = user_id;}
|
||||
public void setContact_id(UUID contact_id){this.contact_id = contact_id;}
|
||||
public void setAdd_at(LocalDateTime add_at){this.added_at =add_at;}
|
||||
|
||||
@@ -1,19 +1,162 @@
|
||||
package org.to.telegramfinalproject.Models;
|
||||
|
||||
public class FileAttachment {
|
||||
private String fileUrl;
|
||||
private String fileType;
|
||||
import org.json.JSONObject;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public FileAttachment(String fileUrl, String fileType) {
|
||||
public class FileAttachment {
|
||||
|
||||
private UUID attachmentId; // اختیاری؛ اگر null بود، تولید میکنیم
|
||||
private UUID mediaKey;
|
||||
private String fileUrl;
|
||||
private String fileType; // IMAGE, VIDEO, AUDIO, FILE, GIF, STICKER
|
||||
private String fileName;
|
||||
private Long fileSize;
|
||||
private String mimeType; // e.g., image/png
|
||||
private Integer width;
|
||||
private Integer height;
|
||||
private Integer durationSeconds; // for audio/video
|
||||
private String thumbnailUrl;
|
||||
private String storagePath;
|
||||
|
||||
public FileAttachment(String fileUrl,
|
||||
String fileType,
|
||||
String fileName,
|
||||
Long fileSize,
|
||||
String mimeType,
|
||||
Integer width,
|
||||
Integer height,
|
||||
Integer durationSeconds,
|
||||
String thumbnailUrl) {
|
||||
this.fileUrl = fileUrl;
|
||||
this.fileType = fileType;
|
||||
this.fileName = fileName;
|
||||
this.fileSize = fileSize;
|
||||
this.mimeType = mimeType;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.durationSeconds = durationSeconds;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
public String getFileUrl() {
|
||||
return fileUrl;
|
||||
public FileAttachment(String fileUrl, String fileType) {
|
||||
this(fileUrl, fileType, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
public FileAttachment() {
|
||||
}
|
||||
|
||||
|
||||
// ساخت از JSON /upload
|
||||
public static FileAttachment fromUploadJson(JSONObject j) {
|
||||
return new FileAttachment(
|
||||
j.optString("file_url", ""),
|
||||
j.optString("file_type", "FILE"),
|
||||
emptyToNull(j.optString("file_name", null)),
|
||||
j.has("file_size") && !j.isNull("file_size") ? j.getLong("file_size") : null,
|
||||
emptyToNull(j.optString("mime_type", null)),
|
||||
j.has("width") && !j.isNull("width") ? j.getInt("width") : null,
|
||||
j.has("height") && !j.isNull("height") ? j.getInt("height") : null,
|
||||
j.has("duration_seconds") && !j.isNull("duration_seconds") ? j.getInt("duration_seconds") : null,
|
||||
j.isNull("thumbnail_url") ? null : emptyToNull(j.optString("thumbnail_url", null))
|
||||
);
|
||||
}
|
||||
|
||||
public JSONObject toJson() {
|
||||
JSONObject out = new JSONObject()
|
||||
.put("file_url", fileUrl)
|
||||
.put("file_type", fileType);
|
||||
|
||||
out.put("file_name", fileName == null ? JSONObject.NULL : fileName);
|
||||
out.put("file_size", fileSize == null ? JSONObject.NULL : fileSize);
|
||||
out.put("mime_type", mimeType == null ? JSONObject.NULL : mimeType);
|
||||
out.put("width", width == null ? JSONObject.NULL : width);
|
||||
out.put("height", height == null ? JSONObject.NULL : height);
|
||||
out.put("duration_seconds", durationSeconds == null ? JSONObject.NULL : durationSeconds);
|
||||
out.put("thumbnail_url", thumbnailUrl == null ? JSONObject.NULL : thumbnailUrl);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
public boolean isImage() { return "IMAGE".equalsIgnoreCase(fileType) || "GIF".equalsIgnoreCase(fileType); }
|
||||
public boolean isAudio() { return "AUDIO".equalsIgnoreCase(fileType); }
|
||||
public boolean hasDimensions() { return width != null && height != null; }
|
||||
|
||||
private static String emptyToNull(String s) {
|
||||
return (s == null || s.isBlank()) ? null : s;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getFileUrl() { return fileUrl; }
|
||||
public String getFileType() { return fileType; }
|
||||
public String getFileName() { return fileName; }
|
||||
public Long getFileSize() { return fileSize; }
|
||||
public String getMimeType() { return mimeType; }
|
||||
public Integer getWidth() { return width; }
|
||||
public Integer getHeight() { return height; }
|
||||
public Integer getDurationSeconds() { return durationSeconds; }
|
||||
public String getThumbnailUrl() { return thumbnailUrl; }
|
||||
|
||||
// equals/hashCode/toString
|
||||
@Override public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof FileAttachment)) return false;
|
||||
FileAttachment that = (FileAttachment) o;
|
||||
return Objects.equals(fileUrl, that.fileUrl) &&
|
||||
Objects.equals(fileType, that.fileType) &&
|
||||
Objects.equals(fileName, that.fileName) &&
|
||||
Objects.equals(fileSize, that.fileSize) &&
|
||||
Objects.equals(mimeType, that.mimeType) &&
|
||||
Objects.equals(width, that.width) &&
|
||||
Objects.equals(height, that.height) &&
|
||||
Objects.equals(durationSeconds, that.durationSeconds) &&
|
||||
Objects.equals(thumbnailUrl, that.thumbnailUrl);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Objects.hash(fileUrl, fileType, fileName, fileSize, mimeType, width, height, durationSeconds, thumbnailUrl);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "FileAttachment{" +
|
||||
"fileUrl='" + fileUrl + '\'' +
|
||||
", fileType='" + fileType + '\'' +
|
||||
", fileName='" + fileName + '\'' +
|
||||
", fileSize=" + fileSize +
|
||||
", mimeType='" + mimeType + '\'' +
|
||||
", width=" + width +
|
||||
", height=" + height +
|
||||
", durationSeconds=" + durationSeconds +
|
||||
", thumbnailUrl='" + thumbnailUrl + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public UUID getAttachmentId() {return attachmentId;
|
||||
}
|
||||
|
||||
public UUID getMediaKey() {return mediaKey;
|
||||
}
|
||||
|
||||
public String getStoragePath() {return storagePath;
|
||||
}
|
||||
|
||||
public void setAttachmentId(UUID attachmentId) {this.attachmentId = attachmentId;
|
||||
}
|
||||
|
||||
public void setMediaKey(UUID mediaKey) {this.mediaKey = mediaKey;
|
||||
}
|
||||
|
||||
|
||||
public void setFileUrl(String fileUrl) {this.fileUrl = fileUrl;
|
||||
}
|
||||
public void setFileType(String fileType){this.fileType = fileType;}
|
||||
public void setFileName(String fileName){this.fileName = fileName;}
|
||||
public void setFileSize(Long fileSize){this.fileSize = fileSize;}
|
||||
public void setMimeType(String mimeType){this.mimeType = mimeType;}
|
||||
public void setWidth(int width){this.width = width;}
|
||||
public void setHeight(int height){this.height = height;}
|
||||
public void setDurationSeconds(Integer durationSeconds){this.durationSeconds = durationSeconds;}
|
||||
public void setThumbnailUrl(String thumbnailUrl){this.thumbnailUrl = thumbnailUrl;}
|
||||
public void setStoragePath(String storagePath) {this.storagePath = storagePath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.to.telegramfinalproject.Models;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class MediaRow {
|
||||
public UUID messageId;
|
||||
public String storagePath;
|
||||
public String fileName;
|
||||
public String mimeType;
|
||||
public Long fileSize;
|
||||
public String receiverType;
|
||||
public UUID receiverId;
|
||||
public UUID senderId;
|
||||
public java.util.UUID attachmentId;
|
||||
public java.util.UUID mediaKey;
|
||||
public String fileType; // IMAGE/AUDIO/...
|
||||
public Integer width;
|
||||
public Integer height;
|
||||
public Integer durationSeconds; //for audio only
|
||||
public String thumbnailUrl;
|
||||
public String fileUrl; //display link
|
||||
public String chatType;
|
||||
public UUID chatId;
|
||||
}
|
||||
@@ -12,6 +12,10 @@ import org.to.telegramfinalproject.Utils.GroupPermissionUtil;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@@ -20,6 +24,15 @@ public class ClientHandler implements Runnable {
|
||||
private final AuthService authService = new AuthService();
|
||||
private User currentUser;
|
||||
|
||||
// ClientHandler.java
|
||||
private static void log(String msg) {
|
||||
System.out.println(java.time.LocalDateTime.now() + " [ClientHandler] " + msg);
|
||||
}
|
||||
private static void logf(String fmt, Object... args) {
|
||||
log(String.format(fmt, args));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ClientHandler(Socket socket) {
|
||||
this.socket = socket;
|
||||
@@ -30,11 +43,60 @@ public class ClientHandler implements Runnable {
|
||||
UUID userId = null;
|
||||
|
||||
try (
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
|
||||
|
||||
// InputStream rawIn = socket.getInputStream();
|
||||
// OutputStream rawOut = socket.getOutputStream();
|
||||
//
|
||||
// BufferedReader in = new BufferedReader(new InputStreamReader(rawIn, java.nio.charset.StandardCharsets.UTF_8));
|
||||
// PrintWriter out = new PrintWriter(new OutputStreamWriter(rawOut, java.nio.charset.StandardCharsets.UTF_8), true);
|
||||
|
||||
// DataInputStream dis = new DataInputStream(rawIn);
|
||||
// DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(rawOut));
|
||||
|
||||
// BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
// PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
|
||||
// BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
|
||||
// DataInputStream dis = new DataInputStream(bis); //for binary headers
|
||||
|
||||
//PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), java.nio.charset.StandardCharsets.UTF_8), true);
|
||||
InputStream rawIn = socket.getInputStream();
|
||||
OutputStream rawOut = socket.getOutputStream();
|
||||
|
||||
BufferedInputStream bis = new BufferedInputStream(rawIn);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(rawOut);
|
||||
|
||||
DataInputStream dis = new DataInputStream(bis);
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
PrintWriter out = new PrintWriter(new OutputStreamWriter(bos, java.nio.charset.StandardCharsets.UTF_8), true);
|
||||
|
||||
) {
|
||||
|
||||
// DataInputStream bin = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
|
||||
|
||||
String inputLine;
|
||||
while ((inputLine = in.readLine()) != null) {
|
||||
while ((inputLine = readUtf8Line(bis)) != null) {
|
||||
|
||||
String line = inputLine.trim();
|
||||
|
||||
if ("MEDIA".equalsIgnoreCase(inputLine.trim())) {
|
||||
handleMediaFrame(dis, out);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if ("MEDIA_DL".equalsIgnoreCase(line)) {
|
||||
UUID cu = (currentUser == null ? null : currentUser.getInternal_uuid());
|
||||
logf("MEDIA_DL received. currentUser.internal_uuid=%s", cu);
|
||||
|
||||
if (cu == null) {
|
||||
log("MEDIA_DL rejected: currentUser is null or no internal_uuid");
|
||||
sendDlErr(dos, "not authorized");
|
||||
continue;
|
||||
}
|
||||
handleMediaDownload(dis, dos, cu);
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject requestJson = new JSONObject(inputLine);
|
||||
String action = requestJson.getString("action");
|
||||
ResponseModel response = null;
|
||||
@@ -2275,6 +2337,13 @@ public class ClientHandler implements Runnable {
|
||||
|
||||
List<Message> messages = MessageDatabase.getMessagesForChat(chatId, chatType, currentUser.getInternal_uuid(), offset, limit);
|
||||
|
||||
java.util.List<UUID> mids = new java.util.ArrayList<>();
|
||||
for (Message m : messages) mids.add(m.getMessage_id());
|
||||
|
||||
// ⬅️ همهٔ اتچمنتها را یکجا بگیر: message_id -> list(attachments)
|
||||
java.util.Map<UUID, java.util.List<MediaRow>> attMap =
|
||||
MessageDatabase.findAttachmentsForMessages(mids);
|
||||
|
||||
JSONArray result = new JSONArray();
|
||||
for (Message m : messages) {
|
||||
JSONObject obj = new JSONObject();
|
||||
@@ -2342,6 +2411,26 @@ public class ClientHandler implements Runnable {
|
||||
obj.put("reactions", new JSONArray(reactions));
|
||||
|
||||
|
||||
JSONArray atts = new JSONArray();
|
||||
java.util.List<MediaRow> list = attMap.getOrDefault(m.getMessage_id(), java.util.Collections.emptyList());
|
||||
for (MediaRow a : list) {
|
||||
JSONObject aj = new JSONObject()
|
||||
.put("media_key", a.mediaKey != null ? a.mediaKey.toString() : JSONObject.NULL)
|
||||
.put("file_name", a.fileName != null ? a.fileName : JSONObject.NULL)
|
||||
.put("file_size", a.fileSize != null ? a.fileSize : JSONObject.NULL)
|
||||
.put("mime_type", a.mimeType != null ? a.mimeType : JSONObject.NULL)
|
||||
.put("file_type", a.fileType != null ? a.fileType : JSONObject.NULL)
|
||||
.put("width", a.width != null ? a.width : JSONObject.NULL)
|
||||
.put("height", a.height != null ? a.height : JSONObject.NULL)
|
||||
.put("duration_seconds", a.durationSeconds != null ? a.durationSeconds : JSONObject.NULL)
|
||||
.put("thumbnail_url", a.thumbnailUrl != null ? a.thumbnailUrl : JSONObject.NULL)
|
||||
// اختیاری/دیباگ
|
||||
.put("file_url", a.fileUrl != null ? a.fileUrl : JSONObject.NULL);
|
||||
atts.put(aj);
|
||||
}
|
||||
obj.put("attachments", atts);
|
||||
|
||||
|
||||
result.put(obj);
|
||||
}
|
||||
|
||||
@@ -2870,7 +2959,9 @@ public class ClientHandler implements Runnable {
|
||||
userDatabase.updateLastSeen(userId);
|
||||
SessionManager.removeUser(userId);
|
||||
}
|
||||
} finally {
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
try {
|
||||
if (currentUser != null) {
|
||||
//RealTime
|
||||
@@ -2895,9 +2986,606 @@ public class ClientHandler implements Runnable {
|
||||
|
||||
}
|
||||
|
||||
private static String readUtf8Line(BufferedInputStream bis) throws java.io.IOException {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (true) {
|
||||
int b = bis.read();
|
||||
if (b == -1) {
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
if (b == '\n') {
|
||||
int len = sb.length();
|
||||
if (len > 0 && sb.charAt(len - 1) == '\r') sb.setLength(len - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
sb.append((char) b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// private void handleMediaFrame(DataInputStream dis, PrintWriter out) {
|
||||
// try {
|
||||
// // MAGIC = "MDM1"
|
||||
// final int MAGIC_EXPECTED = 0x4D444D31;
|
||||
// int magic = dis.readInt();
|
||||
// if (magic != MAGIC_EXPECTED) {
|
||||
// out.println(new JSONObject().put("status","error").put("message","bad magic").toString());
|
||||
// out.flush();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// int headerLen = dis.readInt();
|
||||
// if (headerLen <= 0 || headerLen > (64 * 1024)) {
|
||||
// out.println(new JSONObject().put("status","error").put("message","bad header length").toString());
|
||||
// out.flush();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// byte[] headerBytes = dis.readNBytes(headerLen);
|
||||
// if (headerBytes.length != headerLen) {
|
||||
// out.println(new JSONObject().put("status","error").put("message","header truncated").toString());
|
||||
// out.flush();
|
||||
// return;
|
||||
// }
|
||||
// JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8));
|
||||
//
|
||||
// long contentLen = dis.readLong();
|
||||
// long MAX_MEDIA = 25L * 1024 * 1024;
|
||||
// if (contentLen <= 0 || contentLen > MAX_MEDIA) {
|
||||
// skip(dis, contentLen);
|
||||
// out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString());
|
||||
// out.flush();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// UUID messageId = UUID.fromString(h.getString("message_id"));
|
||||
// UUID senderId = UUID.fromString(h.getString("sender_id"));
|
||||
// String rType = h.getString("receiver_type"); // private/group/channel
|
||||
// UUID receiverId = UUID.fromString(h.getString("receiver_id"));
|
||||
// String messageType = h.getString("message_type"); // IMAGE | AUDIO
|
||||
//
|
||||
// if (!"IMAGE".equalsIgnoreCase(messageType) && !"AUDIO".equalsIgnoreCase(messageType)) {
|
||||
// skip(dis, contentLen);
|
||||
// out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString());
|
||||
// out.flush();
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// String fileName = h.optString("file_name", "file.bin");
|
||||
// String mimeType = h.optString("mime_type", "application/octet-stream");
|
||||
// String text = h.optString("text", "");
|
||||
//
|
||||
// Integer width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null;
|
||||
// Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null;
|
||||
//
|
||||
// if (fileName.length() > 200) fileName = fileName.substring(0, 200);
|
||||
//
|
||||
// // مسیر ذخیره
|
||||
// java.nio.file.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize();
|
||||
// java.nio.file.Files.createDirectories(baseDir);
|
||||
// String kind = "IMAGE".equalsIgnoreCase(messageType) ? "images" : "audios";
|
||||
// String subdir = kind + "/" + java.time.LocalDate.now();
|
||||
// java.nio.file.Path dir = baseDir.resolve(subdir).normalize();
|
||||
// java.nio.file.Files.createDirectories(dir);
|
||||
//
|
||||
// String ext = guessExt(fileName, mimeType);
|
||||
// String storedName = java.util.UUID.randomUUID() + ext;
|
||||
// java.nio.file.Path target = dir.resolve(storedName).normalize();
|
||||
//
|
||||
// // دریافت بایتهای فایل
|
||||
// try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream(
|
||||
// target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) {
|
||||
// long remaining = contentLen;
|
||||
// byte[] buf = new byte[8192];
|
||||
// while (remaining > 0) {
|
||||
// int toRead = (int) Math.min(buf.length, remaining);
|
||||
// int n = dis.read(buf, 0, toRead);
|
||||
// if (n == -1) throw new EOFException("stream ended early");
|
||||
// fos.write(buf, 0, n);
|
||||
// remaining -= n;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// long fileSize = java.nio.file.Files.size(target);
|
||||
// String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName;
|
||||
//
|
||||
// FileAttachment att = new FileAttachment(
|
||||
// fileUrl,
|
||||
// messageType.toUpperCase(), // IMAGE/AUDIO
|
||||
// fileName,
|
||||
// fileSize,
|
||||
// mimeType,
|
||||
// width,
|
||||
// height,
|
||||
// null, // durationSeconds
|
||||
// null // thumbnailUrl
|
||||
// );
|
||||
//
|
||||
// boolean ok = MessageDatabase.saveMessageWithOptionalAttachments(
|
||||
// messageId, senderId, receiverId, rType, text, messageType.toUpperCase(), java.util.List.of(att)
|
||||
// );
|
||||
//
|
||||
// JSONObject ack = new JSONObject()
|
||||
// .put("status", ok ? "success" : "error")
|
||||
// .put("message_id", messageId.toString())
|
||||
// .put("file_url", fileUrl)
|
||||
// .put("file_size", fileSize)
|
||||
// .put("mime_type", mimeType);
|
||||
//
|
||||
// out.println(ack.toString());
|
||||
// out.flush();
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// out.println(new JSONObject().put("status","error").put("message","exception").toString());
|
||||
// out.flush();
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
private void handleMediaFrame(DataInputStream dis, PrintWriter out) {
|
||||
try {
|
||||
final int MAGIC_EXPECTED = 0x4D444D31; // "MDM1"
|
||||
int magic = dis.readInt();
|
||||
if (magic != MAGIC_EXPECTED) {
|
||||
out.println(new JSONObject().put("status","error").put("message","bad magic").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
int headerLen = dis.readInt();
|
||||
if (headerLen <= 0 || headerLen > 64 * 1024) {
|
||||
out.println(new JSONObject().put("status","error").put("message","bad header length").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
byte[] headerBytes = dis.readNBytes(headerLen);
|
||||
if (headerBytes.length != headerLen) {
|
||||
out.println(new JSONObject().put("status","error").put("message","header truncated").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
JSONObject h = new JSONObject(new String(headerBytes, java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
long contentLen = dis.readLong();
|
||||
long MAX_MEDIA = 25L * 1024 * 1024;
|
||||
if (contentLen <= 0 || contentLen > MAX_MEDIA) {
|
||||
skip(dis, contentLen);
|
||||
out.println(new JSONObject().put("status","error").put("message","file too large/invalid").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
UUID messageId = UUID.fromString(h.getString("message_id"));
|
||||
UUID senderId = UUID.fromString(h.getString("sender_id"));
|
||||
String rType = h.getString("receiver_type"); // private/group/channel
|
||||
UUID receiverId = UUID.fromString(h.getString("receiver_id"));
|
||||
String messageType = h.getString("message_type").toUpperCase(); // IMAGE | AUDIO
|
||||
|
||||
if (!"IMAGE".equals(messageType) && !"AUDIO".equals(messageType)) {
|
||||
skip(dis, contentLen);
|
||||
out.println(new JSONObject().put("status","error").put("message","unsupported message_type").toString()); out.flush(); return;
|
||||
}
|
||||
|
||||
String fileName = h.optString("file_name", "file.bin");
|
||||
String mimeType = h.optString("mime_type", "application/octet-stream");
|
||||
String text = h.optString("text", ""); // کپشن اختیاری
|
||||
|
||||
Integer width = h.has("width") && !h.isNull("width") ? h.getInt("width") : null;
|
||||
Integer height = h.has("height") && !h.isNull("height") ? h.getInt("height") : null;
|
||||
|
||||
if (fileName.length() > 200) fileName = fileName.substring(0, 200);
|
||||
|
||||
java.nio.file.Path baseDir = java.nio.file.Paths.get("uploads").toAbsolutePath().normalize();
|
||||
java.nio.file.Files.createDirectories(baseDir);
|
||||
String kind = "IMAGE".equals(messageType) ? "images" : "audios";
|
||||
String subdir = kind + "/" + java.time.LocalDate.now();
|
||||
java.nio.file.Path dir = baseDir.resolve(subdir).normalize();
|
||||
java.nio.file.Files.createDirectories(dir);
|
||||
|
||||
String ext = guessExt(fileName, mimeType);
|
||||
String storedName = java.util.UUID.randomUUID() + ext;
|
||||
java.nio.file.Path target = dir.resolve(storedName).normalize();
|
||||
|
||||
try (OutputStream fos = new BufferedOutputStream(java.nio.file.Files.newOutputStream(
|
||||
target, java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.TRUNCATE_EXISTING))) {
|
||||
long remaining = contentLen;
|
||||
byte[] buf = new byte[8192];
|
||||
while (remaining > 0) {
|
||||
int toRead = (int) Math.min(buf.length, remaining);
|
||||
int n = dis.read(buf, 0, toRead);
|
||||
if (n == -1) throw new EOFException("stream ended early");
|
||||
fos.write(buf, 0, n);
|
||||
remaining -= n;
|
||||
}
|
||||
}
|
||||
|
||||
long fileSize = java.nio.file.Files.size(target);
|
||||
|
||||
String storagePath = target.toString();
|
||||
String fileUrl = "/" + subdir.replace('\\','/') + "/" + storedName;
|
||||
String mt = messageType; // "IMAGE" یا "AUDIO"
|
||||
int safeWidth = ("IMAGE".equals(mt) && width != null) ? width : 0;
|
||||
int safeHeight = ("IMAGE".equals(mt) && height != null) ? height : 0;
|
||||
FileAttachment att = new FileAttachment();
|
||||
att.setFileUrl(fileUrl);
|
||||
att.setFileType(messageType); // IMAGE/AUDIO
|
||||
att.setFileName(fileName);
|
||||
att.setFileSize(fileSize);
|
||||
att.setMimeType(mimeType);
|
||||
att.setWidth(safeWidth);
|
||||
att.setHeight(safeHeight);
|
||||
att.setDurationSeconds(0);
|
||||
att.setThumbnailUrl(null);
|
||||
att.setStoragePath(storagePath);
|
||||
java.util.List<FileAttachment> atts = java.util.List.of(att);
|
||||
|
||||
boolean ok = MessageDatabase.saveMessageWithOptionalAttachments(
|
||||
messageId, senderId, receiverId, rType, text, messageType, atts
|
||||
);
|
||||
|
||||
UUID mediaKey = null;
|
||||
try (PreparedStatement q = ConnectionDb.connect().prepareStatement(
|
||||
"SELECT media_key FROM message_attachments WHERE message_id = ? AND storage_path = ? LIMIT 1"
|
||||
)) {
|
||||
q.setObject(1, messageId);
|
||||
q.setString(2, storagePath);
|
||||
try (ResultSet rs = q.executeQuery()) {
|
||||
if (rs.next()) mediaKey = (UUID) rs.getObject(1);
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
sqle.printStackTrace();
|
||||
}
|
||||
|
||||
JSONObject ack = new JSONObject()
|
||||
.put("status", ok ? "success" : "error")
|
||||
.put("message_id", messageId.toString())
|
||||
.put("media_key", mediaKey != null ? mediaKey.toString() : JSONObject.NULL)
|
||||
.put("file_name", fileName)
|
||||
.put("file_size", fileSize)
|
||||
.put("mime_type", mimeType)
|
||||
.put("display_path", fileUrl);
|
||||
|
||||
out.println(ack.toString());
|
||||
out.flush();
|
||||
|
||||
|
||||
// بعد از out.flush(); و فقط اگر ok==true
|
||||
if (ok) {
|
||||
try {
|
||||
// 1) دریافت پیام از DB تا send_at و... دقیق باشد
|
||||
Message m = MessageDatabase.findById(messageId); // اگر چنین متدی نداری، با پارامترهای همین متد بساز/پر کن
|
||||
|
||||
// 2) لیست دریافتکنندگان بر اساس نوع چت
|
||||
List<UUID> receivers = getReceiversForChat(receiverId, rType.toLowerCase());
|
||||
|
||||
|
||||
// 3) ساخت payload شامل اتچمنت (media)
|
||||
User sender = userDatabase.findByInternalUUID(senderId);
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("action", "new_message")
|
||||
.put("data", new JSONObject()
|
||||
.put("id", m.getMessage_id().toString())
|
||||
.put("chat_id", receiverId.toString())
|
||||
.put("chat_type", rType.toLowerCase())
|
||||
.put("sender_id", senderId.toString())
|
||||
.put("sender_name", sender != null ? sender.getProfile_name() : JSONObject.NULL)
|
||||
.put("message_type", messageType.toLowerCase())
|
||||
.put("text", (text == null || text.isEmpty()) ? JSONObject.NULL : text)
|
||||
.put("media", new JSONObject()
|
||||
.put("media_id", mediaKey != null ? mediaKey.toString() : JSONObject.NULL)
|
||||
.put("file_name", fileName)
|
||||
.put("mime_type", mimeType)
|
||||
.put("size_bytes", fileSize)
|
||||
.put("url", fileUrl)
|
||||
.put("thumbnail_url", JSONObject.NULL)
|
||||
.put("width", safeWidth)
|
||||
.put("height", safeHeight)
|
||||
.put("duration_ms", 0)
|
||||
)
|
||||
.put("send_at", m.getSend_at().toString())
|
||||
.put("status", "SENT")
|
||||
);
|
||||
|
||||
// 4) ارسال به همه اعضا (از جمله خودِ فرستنده اگر میخواهی UI آن هم یکپارچه آپدیت شود)
|
||||
for (UUID uid : receivers) {
|
||||
RealTimeEventDispatcher.sendToUser(uid, payload);
|
||||
}
|
||||
|
||||
// (اختیاری) رویداد آپدیت چتلیست برای sort بر اساس آخرین پیام
|
||||
RealTimeEventDispatcher.notifyChatUpdated(receiverId, rType, m);
|
||||
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
// اگر ذخیره شد ولی Broadcast شکست خورد، میتوانی Log کنی یا Retry سبک انجام دهی
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
out.println(new JSONObject().put("status","error").put("message","exception").toString());
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static final int MAGIC_DL = 0x4D444D32; // "MDM2"
|
||||
|
||||
// private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) {
|
||||
// try {
|
||||
// int magic = inBin.readInt();
|
||||
// if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; }
|
||||
//
|
||||
// int hlen = inBin.readInt();
|
||||
// if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; }
|
||||
//
|
||||
// byte[] hb = inBin.readNBytes(hlen);
|
||||
// if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; }
|
||||
//
|
||||
// JSONObject hdr = new JSONObject(new String(hb, java.nio.charset.StandardCharsets.UTF_8));
|
||||
// if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; }
|
||||
//
|
||||
// UUID mediaKey = UUID.fromString(hdr.getString("media_key"));
|
||||
// long offset = Math.max(0L, hdr.optLong("offset", 0L));
|
||||
//
|
||||
// MediaRow mr = MessageDatabase.findMediaByKey(mediaKey);
|
||||
// if (mr == null) { sendDlErr(outBin, "not found"); return; }
|
||||
// if (!MessageDatabase.canAccess(requesterId, mr)) { sendDlErr(outBin, "not authorized"); return; }
|
||||
//
|
||||
// java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize();
|
||||
// long size = java.nio.file.Files.size(path);
|
||||
// if (offset > size) offset = 0L;
|
||||
//
|
||||
// JSONObject ok = new JSONObject()
|
||||
// .put("status","success")
|
||||
// .put("media_key", mediaKey.toString())
|
||||
// .put("file_name", mr.fileName)
|
||||
// .put("mime_type", mr.mimeType)
|
||||
// .put("file_size", size);
|
||||
//
|
||||
// byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
//
|
||||
// outBin.writeInt(MAGIC_DL);
|
||||
// outBin.writeInt(okb.length);
|
||||
// outBin.write(okb);
|
||||
// outBin.writeLong(size - offset);
|
||||
//
|
||||
// try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) {
|
||||
// if (offset > 0) fis.skipNBytes(offset);
|
||||
// byte[] buf = new byte[8192];
|
||||
// long remain = size - offset;
|
||||
// while (remain > 0) {
|
||||
// int n = fis.read(buf, 0, (int) Math.min(buf.length, remain));
|
||||
// if (n == -1) break;
|
||||
// outBin.write(buf, 0, n);
|
||||
// remain -= n;
|
||||
// }
|
||||
// }
|
||||
// outBin.flush();
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {}
|
||||
// }
|
||||
// }
|
||||
|
||||
private void handleMediaDownload(DataInputStream inBin, DataOutputStream outBin, UUID requesterId) {
|
||||
try {
|
||||
logf("MEDIA_DL start. requester=%s", requesterId);
|
||||
|
||||
int magic = inBin.readInt();
|
||||
if (magic != MAGIC_DL) { sendDlErr(outBin, "bad magic"); return; }
|
||||
|
||||
int hlen = inBin.readInt();
|
||||
if (hlen <= 0 || hlen > 64 * 1024) { sendDlErr(outBin, "bad header length"); return; }
|
||||
|
||||
byte[] hb = inBin.readNBytes(hlen);
|
||||
if (hb.length != hlen) { sendDlErr(outBin, "header truncated"); return; }
|
||||
|
||||
String hdrStr = new String(hb, java.nio.charset.StandardCharsets.UTF_8);
|
||||
logf("MEDIA_DL header: %s", hdrStr);
|
||||
|
||||
JSONObject hdr = new JSONObject(hdrStr);
|
||||
if (!"download".equalsIgnoreCase(hdr.optString("op"))) { sendDlErr(outBin, "bad op"); return; }
|
||||
|
||||
UUID mediaKey = UUID.fromString(hdr.getString("media_key"));
|
||||
long offset = Math.max(0L, hdr.optLong("offset", 0L));
|
||||
logf("Parsed mediaKey=%s offset=%d", mediaKey, offset);
|
||||
|
||||
MediaRow mr = MessageDatabase.findMediaByKey(mediaKey);
|
||||
if (mr == null) { sendDlErr(outBin, "not found"); return; }
|
||||
|
||||
logf("MediaRow: chatType=%s chatId=%s sender=%s receiver=%s storage=%s",
|
||||
mr.chatType, mr.chatId, mr.senderId, mr.receiverId, mr.storagePath);
|
||||
|
||||
try (java.sql.Connection c = ConnectionDb.connect();
|
||||
java.sql.PreparedStatement st = c.prepareStatement(
|
||||
"SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1")) {
|
||||
st.setObject(1, mr.chatId, java.sql.Types.OTHER);
|
||||
st.setObject(2, requesterId, java.sql.Types.OTHER);
|
||||
boolean direct;
|
||||
try (java.sql.ResultSet r = st.executeQuery()) { direct = r.next(); }
|
||||
logf("[DL] direct channel membership ch=%s user=%s => %s", mr.chatId, requesterId, direct);
|
||||
} catch (Exception e) {
|
||||
logf("[DL] direct membership check ERROR: %s", e.toString());
|
||||
}
|
||||
|
||||
boolean allowed = MessageDatabase.canAccess(requesterId, mr);
|
||||
logf("canAccess(..) -> %s", allowed);
|
||||
if (!allowed) { sendDlErr(outBin, "not authorized"); return; }
|
||||
|
||||
java.nio.file.Path path = java.nio.file.Paths.get(mr.storagePath).normalize();
|
||||
long size = java.nio.file.Files.size(path);
|
||||
if (offset > size) offset = 0L;
|
||||
|
||||
JSONObject ok = new JSONObject()
|
||||
.put("status","success")
|
||||
.put("media_key", mediaKey.toString())
|
||||
.put("file_name", mr.fileName)
|
||||
.put("mime_type", mr.mimeType)
|
||||
.put("file_size", size);
|
||||
|
||||
byte[] okb = ok.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
outBin.writeInt(MAGIC_DL);
|
||||
outBin.writeInt(okb.length);
|
||||
outBin.write(okb);
|
||||
outBin.writeLong(size - offset);
|
||||
logf("Sending OK header. file=%s size=%d offset=%d", mr.fileName, size, offset);
|
||||
|
||||
try (java.io.InputStream fis = new java.io.BufferedInputStream(java.nio.file.Files.newInputStream(path))) {
|
||||
if (offset > 0) fis.skipNBytes(offset);
|
||||
byte[] buf = new byte[8192];
|
||||
long remain = size - offset;
|
||||
while (remain > 0) {
|
||||
int n = fis.read(buf, 0, (int) Math.min(buf.length, remain));
|
||||
if (n == -1) break;
|
||||
outBin.write(buf, 0, n);
|
||||
remain -= n;
|
||||
}
|
||||
}
|
||||
outBin.flush();
|
||||
log("MEDIA_DL done.");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
try { sendDlErr(outBin, "exception"); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDlErr(DataOutputStream outBin, String msg) throws java.io.IOException {
|
||||
JSONObject j = new JSONObject().put("status","error").put("message", msg);
|
||||
byte[] b = j.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
outBin.writeInt(MAGIC_DL);
|
||||
outBin.writeInt(b.length);
|
||||
outBin.write(b);
|
||||
outBin.writeLong(0L);
|
||||
outBin.flush();
|
||||
}
|
||||
|
||||
private static void skip(DataInputStream dis, long n) throws IOException {
|
||||
if (n <= 0) return;
|
||||
byte[] buf = new byte[8192];
|
||||
long left = n;
|
||||
while (left > 0) {
|
||||
int toRead = (int) Math.min(buf.length, left);
|
||||
int r = dis.read(buf, 0, toRead);
|
||||
if (r == -1) break; // EOF
|
||||
left -= r;
|
||||
}
|
||||
}
|
||||
|
||||
private static String guessExt(String original, String mime) {
|
||||
if (original != null && original.contains(".")) {
|
||||
String ext = original.substring(original.lastIndexOf('.'));
|
||||
if (ext.length() <= 10) return ext.toLowerCase();
|
||||
}
|
||||
if (mime == null) return "";
|
||||
|
||||
String m = mime.toLowerCase();
|
||||
|
||||
if (m.equals("image/png")) return ".png";
|
||||
if (m.equals("image/jpeg") || m.equals("image/jpg")) return ".jpg";
|
||||
if (m.equals("image/gif")) return ".gif";
|
||||
if (m.equals("image/webp")) return ".webp";
|
||||
|
||||
if (m.equals("audio/mpeg") || m.equals("audio/mp3")) return ".mp3";
|
||||
if (m.equals("audio/ogg")) return ".ogg";
|
||||
if (m.equals("audio/opus")) return ".opus";
|
||||
if (m.equals("audio/wav") || m.equals("audio/x-wav")) return ".wav";
|
||||
if (m.equals("audio/m4a") || m.equals("audio/mp4")) return ".m4a";
|
||||
|
||||
// if (m.equals("video/mp4")) return ".mp4";
|
||||
// if (m.equals("video/webm")) return ".webm";
|
||||
|
||||
// fallback
|
||||
if (m.startsWith("image/")) return "";
|
||||
if (m.startsWith("audio/")) return "";
|
||||
if (m.startsWith("video/")) return "";
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// private ResponseModel handleSendMessage(JSONObject json) {
|
||||
//
|
||||
// try {
|
||||
// if (currentUser == null)
|
||||
// return new ResponseModel("error", "Unauthorized. Please login first.");
|
||||
//
|
||||
// UUID messageId = UUID.randomUUID();
|
||||
// UUID senderId = currentUser.getInternal_uuid();
|
||||
// String receiverType = json.getString("receiver_type");
|
||||
// UUID receiverId;
|
||||
// receiverId = UUID.fromString(json.getString("receiver_id"));
|
||||
//
|
||||
// if(Objects.equals(receiverType, "private")){
|
||||
// PrivateChatDatabase.clearDeletedFlag(senderId, receiverId);
|
||||
// UUID other = PrivateChatDatabase.getOtherParticipant(receiverId, senderId);
|
||||
// if (other == null) {
|
||||
// return new ResponseModel("error", "Invalid private chat.");
|
||||
// }
|
||||
// if (ContactDatabase.isBlocked(senderId, other) || ContactDatabase.isBlocked(other, senderId)) {
|
||||
// return new ResponseModel("error", "You can't message this user (blocked).");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// String content = json.optString("content", "");
|
||||
// String messageType = json.optString("message_type", "TEXT");
|
||||
//
|
||||
// boolean inserted = MessageDatabase.insertMessage(messageId, senderId, receiverId, receiverType, content, messageType);
|
||||
// if (!inserted)
|
||||
// return new ResponseModel("error", "Failed to insert message.");
|
||||
//
|
||||
// if (json.has("attachments")) {
|
||||
// JSONArray attachmentsArray = json.getJSONArray("attachments");
|
||||
// List<FileAttachment> attachments = new ArrayList<>();
|
||||
//
|
||||
// for (int i = 0; i < attachmentsArray.length(); i++) {
|
||||
// JSONObject attJson = attachmentsArray.getJSONObject(i);
|
||||
// attachments.add(new FileAttachment(
|
||||
// attJson.getString("file_url"),
|
||||
// attJson.getString("file_type")
|
||||
// ));
|
||||
// }
|
||||
//
|
||||
// boolean attInserted = MessageDatabase.insertAttachments(messageId, attachments);
|
||||
// if (!attInserted)
|
||||
// return new ResponseModel("error", "Message inserted but failed to attach files.");
|
||||
// }
|
||||
//
|
||||
// // Send real-time message
|
||||
// Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now());
|
||||
// List<UUID> receivers = getReceiversForChat(receiverId, receiverType);
|
||||
// receivers.remove(senderId);
|
||||
// RealTimeEventDispatcher.sendNewMessage(msg, receivers);
|
||||
//
|
||||
// // Update chat list (last_message_time)
|
||||
// JSONObject chatUpdate = new JSONObject();
|
||||
// chatUpdate.put("chat_id", receiverId.toString());
|
||||
// chatUpdate.put("chat_type", receiverType);
|
||||
// chatUpdate.put("last_message_time", LocalDateTime.now().toString());
|
||||
//
|
||||
// JSONObject chatPayload = new JSONObject();
|
||||
// chatPayload.put("action", "chat_updated");
|
||||
// chatPayload.put("data", chatUpdate);
|
||||
//
|
||||
// for (UUID receiver : receivers) {
|
||||
// RealTimeEventDispatcher.sendToUser(receiver, chatPayload);
|
||||
// }
|
||||
//
|
||||
// JSONObject data = new JSONObject();
|
||||
// data.put("message_id", messageId.toString());
|
||||
// return new ResponseModel("success", "Message sent successfully.", data);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// return new ResponseModel("error", "Exception occurred while sending message.");
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
private ResponseModel handleSendMessage(JSONObject json) {
|
||||
|
||||
try {
|
||||
if (currentUser == null)
|
||||
return new ResponseModel("error", "Unauthorized. Please login first.");
|
||||
@@ -2905,57 +3593,71 @@ public class ClientHandler implements Runnable {
|
||||
UUID messageId = UUID.randomUUID();
|
||||
UUID senderId = currentUser.getInternal_uuid();
|
||||
String receiverType = json.getString("receiver_type");
|
||||
UUID receiverId;
|
||||
receiverId = UUID.fromString(json.getString("receiver_id"));
|
||||
UUID receiverId = UUID.fromString(json.getString("receiver_id"));
|
||||
|
||||
if(Objects.equals(receiverType, "private")){
|
||||
PrivateChatDatabase.clearDeletedFlag(senderId, receiverId);
|
||||
UUID other = PrivateChatDatabase.getOtherParticipant(receiverId, senderId);
|
||||
if (other == null) {
|
||||
return new ResponseModel("error", "Invalid private chat.");
|
||||
}
|
||||
if (ContactDatabase.isBlocked(senderId, other) || ContactDatabase.isBlocked(other, senderId)) {
|
||||
return new ResponseModel("error", "You can't message this user (blocked).");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
String content = json.optString("content", "");
|
||||
String messageType = json.optString("message_type", "TEXT");
|
||||
|
||||
boolean inserted = MessageDatabase.insertMessage(messageId, senderId, receiverId, receiverType, content, messageType);
|
||||
if (!inserted)
|
||||
return new ResponseModel("error", "Failed to insert message.");
|
||||
|
||||
// Parse attachments
|
||||
List<FileAttachment> attachments = new ArrayList<>();
|
||||
if (json.has("attachments")) {
|
||||
JSONArray attachmentsArray = json.getJSONArray("attachments");
|
||||
List<FileAttachment> attachments = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < attachmentsArray.length(); i++) {
|
||||
JSONObject attJson = attachmentsArray.getJSONObject(i);
|
||||
JSONArray arr = json.getJSONArray("attachments");
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONObject a = arr.getJSONObject(i);
|
||||
attachments.add(new FileAttachment(
|
||||
attJson.getString("file_url"),
|
||||
attJson.getString("file_type")
|
||||
a.optString("file_url",""),
|
||||
a.optString("file_type","FILE"),
|
||||
a.optString("file_name",""),
|
||||
a.has("file_size") && !a.isNull("file_size") ? a.getLong("file_size") : null,
|
||||
a.optString("mime_type", null),
|
||||
a.has("width") && !a.isNull("width") ? a.getInt("width") : null,
|
||||
a.has("height") && !a.isNull("height") ? a.getInt("height") : null,
|
||||
a.has("duration_seconds") && !a.isNull("duration_seconds") ? a.getInt("duration_seconds") : null,
|
||||
a.isNull("thumbnail_url") ? null : a.optString("thumbnail_url", null)
|
||||
));
|
||||
}
|
||||
|
||||
boolean attInserted = MessageDatabase.insertAttachments(messageId, attachments);
|
||||
if (!attInserted)
|
||||
return new ResponseModel("error", "Message inserted but failed to attach files.");
|
||||
}
|
||||
|
||||
// Send real-time message
|
||||
Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now());
|
||||
List<UUID> receivers = getReceiversForChat(receiverId, receiverType);
|
||||
receivers.remove(senderId);
|
||||
RealTimeEventDispatcher.sendNewMessage(msg, receivers);
|
||||
if ((content == null || content.isBlank()) && attachments.isEmpty()) {
|
||||
return new ResponseModel("error", "Empty message: no content or attachment.");
|
||||
}
|
||||
|
||||
// Update chat list (last_message_time)
|
||||
JSONObject chatUpdate = new JSONObject();
|
||||
chatUpdate.put("chat_id", receiverId.toString());
|
||||
chatUpdate.put("chat_type", receiverType);
|
||||
chatUpdate.put("last_message_time", LocalDateTime.now().toString());
|
||||
// Harmonize message_type
|
||||
if (!attachments.isEmpty()) {
|
||||
String firstType = attachments.get(0).getFileType();
|
||||
if ("TEXT".equalsIgnoreCase(messageType)) {
|
||||
messageType = firstType;
|
||||
} else if (!messageType.equalsIgnoreCase(firstType) && !messageType.equalsIgnoreCase("FILE")) {
|
||||
return new ResponseModel("error", "message_type and attachment.file_type mismatch.");
|
||||
}
|
||||
}
|
||||
|
||||
// DB transaction
|
||||
try (Connection conn = ConnectionDb.connect()) {
|
||||
conn.setAutoCommit(false);
|
||||
|
||||
boolean inserted = MessageDatabase.insertMessageTx(conn, messageId, senderId, receiverId, receiverType, content, messageType);
|
||||
if (!inserted) {
|
||||
conn.rollback();
|
||||
return new ResponseModel("error", "Failed to insert message.");
|
||||
}
|
||||
|
||||
if (!attachments.isEmpty()) {
|
||||
boolean attInserted = MessageDatabase.insertAttachmentsTx(conn, messageId, attachments);
|
||||
if (!attInserted) {
|
||||
conn.rollback();
|
||||
return new ResponseModel("error", "Message inserted but failed to attach files.");
|
||||
}
|
||||
}
|
||||
|
||||
conn.commit();
|
||||
}
|
||||
|
||||
// Real-Time
|
||||
Message msg = new Message(messageId, senderId, receiverId, receiverType, content, messageType, LocalDateTime.now());
|
||||
|
||||
JSONObject chatPayload = new JSONObject();
|
||||
chatPayload.put("action", "chat_updated");
|
||||
@@ -2967,9 +3669,76 @@ public class ClientHandler implements Runnable {
|
||||
|
||||
RealTimeEventDispatcher.sendToUser(senderId, chatPayload);
|
||||
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("action", "new_message");
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("message_id", messageId.toString());
|
||||
return new ResponseModel("success", "Message sent successfully.", data);
|
||||
data.put("id", messageId.toString());
|
||||
data.put("sender_id", senderId.toString());
|
||||
data.put("receiver_id", receiverId.toString());
|
||||
data.put("receiver_type", receiverType);
|
||||
data.put("content", content);
|
||||
data.put("message_type", messageType);
|
||||
data.put("send_at", msg.getSend_at().toString());
|
||||
|
||||
if (!attachments.isEmpty()) {
|
||||
JSONArray out = new JSONArray();
|
||||
for (FileAttachment a : attachments) {
|
||||
JSONObject ao = new JSONObject()
|
||||
.put("file_url", a.getFileUrl())
|
||||
.put("file_type", a.getFileType())
|
||||
.put("file_name", a.getFileName() == null ? JSONObject.NULL : a.getFileName())
|
||||
.put("file_size", a.getFileSize() == null ? JSONObject.NULL : a.getFileSize())
|
||||
.put("mime_type", a.getMimeType() == null ? JSONObject.NULL : a.getMimeType())
|
||||
.put("width", a.getWidth() == null ? JSONObject.NULL : a.getWidth())
|
||||
.put("height", a.getHeight() == null ? JSONObject.NULL : a.getHeight())
|
||||
.put("duration_seconds", a.getDurationSeconds() == null ? JSONObject.NULL : a.getDurationSeconds())
|
||||
.put("thumbnail_url", a.getThumbnailUrl() == null ? JSONObject.NULL : a.getThumbnailUrl());
|
||||
out.put(ao);
|
||||
}
|
||||
data.put("attachments", out);
|
||||
}
|
||||
|
||||
User sender = userDatabase.findByInternalUUID(senderId);
|
||||
if (sender != null) data.put("sender_name", sender.getProfile_name());
|
||||
payload.put("data", data);
|
||||
|
||||
// List<UUID> receivers = getReceiversForChat(receiverId, receiverType);
|
||||
// receivers.remove(senderId);
|
||||
// RealTimeEventDispatcher.broadcastToUsers(receivers, payload);
|
||||
//
|
||||
//
|
||||
//
|
||||
// // chat_updated
|
||||
// JSONObject chatUpdate = new JSONObject()
|
||||
// .put("chat_id", receiverId.toString())
|
||||
// .put("chat_type", receiverType)
|
||||
// .put("last_message_time", LocalDateTime.now().toString());
|
||||
//
|
||||
// JSONObject chatPayload = new JSONObject()
|
||||
// .put("action", "chat_updated")
|
||||
// .put("data", chatUpdate);
|
||||
//
|
||||
// for (UUID r : receivers) RealTimeEventDispatcher.sendToUser(r, chatPayload);
|
||||
|
||||
|
||||
List<UUID> allMembers = getReceiversForChat(receiverId, receiverType); // شامل sender
|
||||
// به همه chat_updated بده
|
||||
JSONObject chatUpdate = new JSONObject()
|
||||
.put("chat_id", receiverId.toString())
|
||||
.put("chat_type", receiverType)
|
||||
.put("last_message_time", LocalDateTime.now().toString());
|
||||
JSONObject chatPayload = new JSONObject()
|
||||
.put("action", "chat_updated")
|
||||
.put("data", chatUpdate);
|
||||
for (UUID u : allMembers) RealTimeEventDispatcher.sendToUser(u, chatPayload);
|
||||
|
||||
List<UUID> others = new ArrayList<>(allMembers);
|
||||
others.remove(senderId);
|
||||
RealTimeEventDispatcher.broadcastToUsers(others, payload);
|
||||
|
||||
|
||||
JSONObject respData = new JSONObject().put("message_id", messageId.toString());
|
||||
return new ResponseModel("success", "Message sent successfully.", respData);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@@ -2978,6 +3747,7 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private List<UUID> getReceiversForChat(UUID receiverId, String receiverType) {
|
||||
switch (receiverType) {
|
||||
case "private":
|
||||
|
||||
@@ -7,7 +7,8 @@ import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
public class MainServer {
|
||||
private static final int PORT = 8000;
|
||||
private static final int PORT = 8080;
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package org.to.telegramfinalproject.Server;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.to.telegramfinalproject.Database.ChannelDatabase;
|
||||
import org.to.telegramfinalproject.Database.ContactDatabase;
|
||||
import org.to.telegramfinalproject.Database.GroupDatabase;
|
||||
import org.to.telegramfinalproject.Database.userDatabase;
|
||||
import org.to.telegramfinalproject.Database.*;
|
||||
import org.to.telegramfinalproject.Models.Message;
|
||||
import org.to.telegramfinalproject.Models.User;
|
||||
|
||||
@@ -12,7 +9,9 @@ import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RealTimeEventDispatcher {
|
||||
@@ -425,4 +424,81 @@ public class RealTimeEventDispatcher {
|
||||
}
|
||||
|
||||
|
||||
public static void notifyChatUpdated(UUID chatId, String chatType, Message lastMsg) {
|
||||
if (chatId == null || chatType == null) return;
|
||||
|
||||
final String type = chatType.toLowerCase(Locale.ROOT);
|
||||
|
||||
List<UUID> receivers;
|
||||
switch (type) {
|
||||
case "private":
|
||||
receivers = PrivateChatDatabase.getMembers(chatId);
|
||||
break;
|
||||
|
||||
case "group":
|
||||
receivers = GroupDatabase.getMemberUUIDs(chatId);
|
||||
break;
|
||||
case "channel":
|
||||
receivers = ChannelDatabase.getSubscriberUUIDs(chatId);
|
||||
break;
|
||||
default:
|
||||
receivers = Collections.emptyList();
|
||||
}
|
||||
if (receivers == null || receivers.isEmpty()) return;
|
||||
|
||||
// 2) ساخت خلاصه آخرین پیام برای نمایش در لیست چت
|
||||
String senderName = null;
|
||||
if (lastMsg != null && lastMsg.getSender_id() != null) {
|
||||
User u = userDatabase.findByInternalUUID(lastMsg.getSender_id());
|
||||
if (u != null) senderName = u.getProfile_name();
|
||||
}
|
||||
|
||||
String messageType = lastMsg != null && lastMsg.getMessage_type() != null
|
||||
? lastMsg.getMessage_type().toLowerCase(Locale.ROOT) : "text";
|
||||
|
||||
// preview ساده: برای مدیا، برچسب کوتاه؛ برای متن، کوتاهسازی
|
||||
String preview;
|
||||
if (!"text".equals(messageType)) {
|
||||
switch (messageType) {
|
||||
case "image": preview = "[Photo]"; break;
|
||||
case "video": preview = "[Video]"; break;
|
||||
case "audio": preview = "[Audio]"; break;
|
||||
case "file": preview = "[File]"; break;
|
||||
default: preview = "[Media]";
|
||||
}
|
||||
} else {
|
||||
String t = lastMsg != null ? nullToEmpty(lastMsg.getContent()) : "";
|
||||
preview = t.length() > 80 ? t.substring(0, 80) + "…" : t;
|
||||
}
|
||||
|
||||
String sendAt = (lastMsg != null && lastMsg.getSend_at() != null)
|
||||
? lastMsg.getSend_at().toString()
|
||||
: java.time.OffsetDateTime.now().toString();
|
||||
|
||||
// 3) payload رویداد chat_updated
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("action", "chat_updated")
|
||||
.put("data", new JSONObject()
|
||||
.put("chat_id", chatId.toString())
|
||||
.put("chat_type", type)
|
||||
.put("last_message", new JSONObject()
|
||||
.put("id", lastMsg != null ? lastMsg.getMessage_id().toString() : JSONObject.NULL)
|
||||
.put("sender_id", lastMsg != null ? lastMsg.getSender_id().toString() : JSONObject.NULL)
|
||||
.put("sender_name", senderName != null ? senderName : JSONObject.NULL)
|
||||
.put("message_type", messageType)
|
||||
.put("preview", preview)
|
||||
.put("send_at", sendAt)
|
||||
)
|
||||
.put("last_message_time", sendAt)
|
||||
.put("update_reason", "new_message") // برای کلاینت مفید است
|
||||
);
|
||||
|
||||
// 4) ارسال به همه اعضای چت
|
||||
for (UUID uid : receivers) {
|
||||
sendToUser(uid, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String s) { return s == null ? "" : s; }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.to.telegramfinalproject.Server;
|
||||
|
||||
import org.to.telegramfinalproject.Database.userDatabase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class TestServer {
|
||||
private static final int SOCKET_PORT = 8000; // سرور سوکت
|
||||
private static final int HTTP_PORT = 8080; // سرور آپلود
|
||||
private static final String UPLOAD_BASE_DIR = "uploads"; // پوشهی ذخیره فایلها
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 1) استارت HTTP Upload در ترد جدا
|
||||
Thread httpThread = new Thread(() -> {
|
||||
try {
|
||||
UploadHttp.start(HTTP_PORT, UPLOAD_BASE_DIR);
|
||||
} catch (IOException e) {
|
||||
System.err.println("Upload HTTP failed to start: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, "upload-http");
|
||||
httpThread.setDaemon(true);
|
||||
httpThread.start();
|
||||
|
||||
// 2) سرور سوکت با Thread Pool
|
||||
ExecutorService pool = Executors.newCachedThreadPool();
|
||||
try (ServerSocket serverSocket = new ServerSocket(SOCKET_PORT)) {
|
||||
System.out.println("Socket server started on port " + SOCKET_PORT);
|
||||
userDatabase.setAllUsersOffline();
|
||||
|
||||
// 3) Shutdown Hook برای خاموشی تمیز
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
System.out.println("\nShutting down...");
|
||||
try { serverSocket.close(); } catch (IOException ignore) {}
|
||||
pool.shutdownNow();
|
||||
userDatabase.setAllUsersOffline();
|
||||
System.out.println("Goodbye.");
|
||||
}));
|
||||
|
||||
// 4) حلقه پذیرش اتصالها
|
||||
while (!serverSocket.isClosed()) {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
clientSocket.setTcpNoDelay(true);
|
||||
System.out.println("New client connected: " + clientSocket.getInetAddress());
|
||||
pool.submit(new ClientHandler(clientSocket));
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("Socket server error: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package org.to.telegramfinalproject.Server;
|
||||
|
||||
import static spark.Spark.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import javax.sound.sampled.*; // برای WAV
|
||||
|
||||
import org.json.JSONObject;
|
||||
import com.mpatric.mp3agic.Mp3File;
|
||||
|
||||
public class UploadHttp {
|
||||
|
||||
public static void start(int httpPort, String baseDir) throws IOException {
|
||||
port(httpPort);
|
||||
|
||||
Path basePath = Paths.get(baseDir).toAbsolutePath().normalize();
|
||||
Files.createDirectories(basePath);
|
||||
staticFiles.externalLocation(basePath.toString());
|
||||
|
||||
post("/upload", (req, res) -> {
|
||||
res.type("application/json");
|
||||
try {
|
||||
long MAX_FILE = 25L * 1024 * 1024; // 25MB
|
||||
req.attribute("org.eclipse.jetty.multipartConfig",
|
||||
new MultipartConfigElement("/tmp", MAX_FILE, MAX_FILE, 0));
|
||||
|
||||
Part filePart = req.raw().getPart("file");
|
||||
if (filePart == null || filePart.getSize() == 0) {
|
||||
res.status(400);
|
||||
return jsonError("empty file");
|
||||
}
|
||||
if (filePart.getSize() > MAX_FILE) {
|
||||
res.status(413);
|
||||
return jsonError("file too large");
|
||||
}
|
||||
|
||||
String mime = filePart.getContentType();
|
||||
if (mime == null) {
|
||||
res.status(415);
|
||||
return jsonError("unknown mime");
|
||||
}
|
||||
|
||||
String original = filePart.getSubmittedFileName();
|
||||
String ext = guessExt(original, mime);
|
||||
String day = LocalDate.now().toString();
|
||||
String typeDir = subdirFor(mime); // images/audios/files
|
||||
String subdir = typeDir + "/" + day;
|
||||
String name = java.util.UUID.randomUUID() + ext;
|
||||
|
||||
Path dir = basePath.resolve(subdir).normalize();
|
||||
Files.createDirectories(dir);
|
||||
Path target = dir.resolve(name).normalize();
|
||||
|
||||
try (InputStream in = filePart.getInputStream()) {
|
||||
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
} finally {
|
||||
filePart.delete();
|
||||
}
|
||||
|
||||
String fileUrl = "/" + subdir.replace('\\', '/') + "/" + name;
|
||||
String fileType = mapToFileType(mime);
|
||||
|
||||
//Meta deta only for audio and image
|
||||
Integer width = null, height = null, durationSeconds = null;
|
||||
String thumbnailUrl = null;
|
||||
|
||||
if ("IMAGE".equals(fileType) || "GIF".equals(fileType)) {
|
||||
int[] wh = imageSize(target);
|
||||
if (wh != null) { width = wh[0]; height = wh[1]; }
|
||||
} else if ("AUDIO".equals(fileType)) {
|
||||
durationSeconds = audioDurationSeconds(target, mime, ext);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
return new JSONObject()
|
||||
.put("file_url", fileUrl)
|
||||
.put("file_type", fileType)
|
||||
.put("file_name", original == null ? "" : safeName(original))
|
||||
.put("file_size", Files.size(target))
|
||||
.put("mime_type", mime)
|
||||
.put("width", width == null ? JSONObject.NULL : width)
|
||||
.put("height", height == null ? JSONObject.NULL : height)
|
||||
.put("duration_seconds", durationSeconds == null ? JSONObject.NULL : durationSeconds)
|
||||
.put("thumbnail_url", JSONObject.NULL)
|
||||
.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
res.status(500);
|
||||
return jsonError("internal error");
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
awaitInitialization();
|
||||
System.out.println("Upload HTTP server on http://localhost:" + httpPort + " baseDir=" + basePath);
|
||||
}
|
||||
|
||||
// ---------- Helpers ----------
|
||||
|
||||
private static String jsonError(String msg) {
|
||||
return new JSONObject().put("error", msg).toString();
|
||||
}
|
||||
|
||||
private static String subdirFor(String mime) {
|
||||
String m = mime.toLowerCase();
|
||||
if (m.startsWith("image/")) return "images";
|
||||
if (m.startsWith("audio/")) return "audios";
|
||||
return "files";
|
||||
}
|
||||
|
||||
private static String mapToFileType(String mime) {
|
||||
String m = mime.toLowerCase();
|
||||
if (m.startsWith("image/")) {
|
||||
if (m.contains("gif")) return "GIF";
|
||||
return "IMAGE";
|
||||
}
|
||||
if (m.startsWith("audio/")) return "AUDIO";
|
||||
return "FILE";
|
||||
}
|
||||
|
||||
private static String guessExt(String original, String mime) {
|
||||
if (original != null && original.contains(".")) {
|
||||
String ext = original.substring(original.lastIndexOf('.'));
|
||||
if (ext.length() <= 10) return ext;
|
||||
}
|
||||
if ("image/png".equalsIgnoreCase(mime)) return ".png";
|
||||
if ("image/jpeg".equalsIgnoreCase(mime)) return ".jpg";
|
||||
if ("image/gif".equalsIgnoreCase(mime)) return ".gif";
|
||||
if ("audio/mpeg".equalsIgnoreCase(mime)) return ".mp3";
|
||||
if ("audio/wav".equalsIgnoreCase(mime) || "audio/x-wav".equalsIgnoreCase(mime)) return ".wav";
|
||||
if ("application/pdf".equalsIgnoreCase(mime)) return ".pdf";
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String safeName(String name) {
|
||||
return name.replace("\"", "").replace("\n", "").replace("\r", "");
|
||||
}
|
||||
|
||||
private static int[] imageSize(Path file) {
|
||||
try {
|
||||
BufferedImage bi = ImageIO.read(file.toFile());
|
||||
if (bi != null) return new int[]{bi.getWidth(), bi.getHeight()};
|
||||
} catch (Exception ignore) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
//only audio
|
||||
private static Integer audioDurationSeconds(Path file, String mime, String ext) {
|
||||
try {
|
||||
if ("audio/mpeg".equalsIgnoreCase(mime) || ".mp3".equalsIgnoreCase(ext)) {
|
||||
Mp3File mp3 = new Mp3File(file.toFile());
|
||||
return (int) mp3.getLengthInSeconds();
|
||||
}
|
||||
|
||||
// WAV با javax.sound.sampled
|
||||
if ("audio/wav".equalsIgnoreCase(mime) || "audio/x-wav".equalsIgnoreCase(mime) || ".wav".equalsIgnoreCase(ext)) {
|
||||
try (AudioInputStream ais = AudioSystem.getAudioInputStream(file.toFile())) {
|
||||
AudioFormat format = ais.getFormat();
|
||||
long frames = ais.getFrameLength();
|
||||
if (frames > 0 && format.getFrameRate() > 0) {
|
||||
double seconds = frames / format.getFrameRate();
|
||||
return (int)Math.round(seconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedAudioFileException | IOException ignore) {
|
||||
// فرمت صوتی پشتیبانی نشده برای AudioSystem
|
||||
} catch (Exception ignore) {
|
||||
// mp3agic یا سایر استثناها
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -73,4 +73,29 @@ public class ChannelPermissionUtil {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean isUserInChannel(UUID userId, UUID channelId) {
|
||||
final String SQL = "SELECT 1 FROM channel_subscribers WHERE channel_id = ? AND user_id = ? LIMIT 1";
|
||||
try (Connection conn = ConnectionDb.connect();
|
||||
PreparedStatement ps = conn.prepareStatement(SQL)) {
|
||||
|
||||
ps.setObject(1, channelId, java.sql.Types.OTHER); // 👈 مهم برای Postgres UUID
|
||||
ps.setObject(2, userId, java.sql.Types.OTHER);
|
||||
|
||||
System.out.println("[SQL] isUserInChannel ch=" + channelId + " user=" + userId
|
||||
+ " db=" + conn.getMetaData().getURL());
|
||||
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
boolean ok = rs.next();
|
||||
System.out.println("[SQL] isUserInChannel -> " + ok);
|
||||
return ok;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -156,5 +156,14 @@ CREATE TABLE IF NOT EXISTS message_attachments (
|
||||
|
||||
);
|
||||
|
||||
--Run this part in your pg
|
||||
ALTER TABLE message_attachments
|
||||
ADD COLUMN IF NOT EXISTS media_key UUID;
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
UPDATE message_attachments
|
||||
SET media_key = gen_random_uuid()
|
||||
WHERE media_key IS NULL;
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user