Work on download files(private chats and groups)
This commit is contained in:
@@ -7,6 +7,9 @@ import org.to.telegramfinalproject.Models.ContactEntry;
|
||||
import org.to.telegramfinalproject.Models.SearchRequestModel;
|
||||
|
||||
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.BlockingQueue;
|
||||
@@ -23,19 +26,11 @@ public class ActionHandler {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void handleRealTime(JSONObject json) throws IOException {
|
||||
IncomingMessageListener listener = new IncomingMessageListener(this.in);
|
||||
listener.handleRealTimeEvent (json);
|
||||
}
|
||||
// public ActionHandler(PrintWriter out, BufferedReader in, Scanner scanner) {
|
||||
// this.out = out;
|
||||
// this.in = in;
|
||||
// this.scanner = scanner;
|
||||
// ActionHandler.instance = this;
|
||||
//
|
||||
// }
|
||||
|
||||
public ActionHandler(PrintWriter out, BufferedReader in, DataOutputStream outBin, Scanner scanner) {
|
||||
this.out = out;
|
||||
this.in = in;
|
||||
@@ -477,6 +472,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");
|
||||
@@ -3648,6 +3649,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: ");
|
||||
@@ -3697,6 +3710,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"));
|
||||
|
||||
@@ -3722,6 +3739,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: ");
|
||||
@@ -3754,6 +3774,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.");
|
||||
}
|
||||
|
||||
@@ -3763,6 +3792,126 @@ public class ActionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
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/<chatDisplayOrId or ChatUUID>/
|
||||
String folderName = (chat.getDisplayId() != null && !chat.getDisplayId().isBlank())
|
||||
? chat.getDisplayId() : chat.getId().toString();
|
||||
Path saveDir = Paths.get(System.getProperty("user.home"), "Downloads", "TeleSock", folderName);
|
||||
|
||||
try { Files.createDirectories(saveDir); } catch (IOException e) {
|
||||
System.out.println("❌ Cannot create folder: " + saveDir + " -> " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// اگر قبلاً دانلود شده (و فایل واقعاً وجود دارد)
|
||||
try {
|
||||
Path existing = Session.downloadsIndex.find(mediaKey);
|
||||
if (existing != null) {
|
||||
System.out.println("✅ Already downloaded: " + existing);
|
||||
return;
|
||||
}
|
||||
} catch (IllegalStateException notInit) {
|
||||
System.out.println("⚠️ DownloadsIndex not initialized. Call DownloadsIndex.init(<internal_uuid>) after login.");
|
||||
// ادامه میدهیم؛ فقط کش نمیشود.
|
||||
}
|
||||
|
||||
// جلوگیری از overwrite با انتخاب نام یکتا
|
||||
Path target = uniquePath(saveDir, fileName);
|
||||
|
||||
|
||||
// دانلود روی همان سوکت: Listener را موقتاً متوقف کن
|
||||
TelegramClient.mediaBusy.set(true);
|
||||
try {
|
||||
Path saved = TelegramClient.getDownloader().download(mediaKey, saveDir, target.getFileName().toString());
|
||||
|
||||
long sizeToRecord = declaredSize > 0 ? declaredSize : Files.size(saved);
|
||||
try {
|
||||
Session.downloadsIndex.put(mediaKey, saved, sizeToRecord);
|
||||
} catch (IllegalStateException notInit) {
|
||||
// اگر init نشده بود، تنها کش نمیکنیم
|
||||
}
|
||||
|
||||
System.out.println("✅ Saved to: " + saved + " (" + humanSize(sizeToRecord) + ")");
|
||||
} catch (Exception ex) {
|
||||
System.out.println("❌ Download failed: " + ex.getMessage());
|
||||
} finally {
|
||||
TelegramClient.mediaBusy.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// نام یکتا اگر فایل موجود است: name.png -> name (1).png
|
||||
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("[\\\\/:*?\"<>|]", "_");
|
||||
// جلوگیری از parent traversal
|
||||
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();
|
||||
@@ -4130,6 +4279,39 @@ public class ActionHandler {
|
||||
}
|
||||
|
||||
|
||||
|
||||
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() {
|
||||
// اولویت: username → user_id → profile_name → internal_uuid
|
||||
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()) {
|
||||
// fallback به displayId یا id
|
||||
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,122 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class IncomingMessageListener implements Runnable {
|
||||
private final BufferedReader in;
|
||||
private volatile boolean running = true;
|
||||
|
||||
public IncomingMessageListener(BufferedReader in) {
|
||||
this.in = in;
|
||||
@@ -21,15 +22,41 @@ 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;
|
||||
}
|
||||
|
||||
// ✅ فقط وقتی دادهٔ متنی آماده است بخوان (بدون بلاک شدن روی readLine)
|
||||
if (!in.ready()) {
|
||||
try { Thread.sleep(10); } catch (InterruptedException ignored) {}
|
||||
continue;
|
||||
}
|
||||
|
||||
String line = in.readLine();
|
||||
if (line == null) {
|
||||
// socket بسته شده
|
||||
break;
|
||||
}
|
||||
|
||||
// خطهای خالی/سفید رو رد کن
|
||||
if (line.isBlank()) continue;
|
||||
|
||||
// تلاش برای پارس JSON
|
||||
final JSONObject response;
|
||||
try {
|
||||
response = new JSONObject(line);
|
||||
} catch (Exception badJson) {
|
||||
// اگر به هر دلیلی خط JSON نبود (مثلاً نویز)، امن رد کن
|
||||
System.out.println("⚠️ [Listener] Non-JSON line ignored: " + line);
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject response = new JSONObject(line);
|
||||
System.out.println("📥 Received raw line: " + line);
|
||||
|
||||
|
||||
//for media
|
||||
// --- Media ACK routing by message_id ---
|
||||
String mid = response.optString("message_id", "");
|
||||
if (!mid.isEmpty()) {
|
||||
BlockingQueue<JSONObject> q = TelegramClient.pendingResponses.get(mid);
|
||||
@@ -39,9 +66,9 @@ public class IncomingMessageListener implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
//if it has reqID answer
|
||||
// --- 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));
|
||||
|
||||
@@ -52,15 +79,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);
|
||||
|
||||
@@ -69,11 +93,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,26 +115,30 @@ 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" -> true;
|
||||
"became_admin", "removed_admin", "ownership_transferred",
|
||||
"admin_permissions_updated", "created_private_chat",
|
||||
"message_reacted", "message_unreacted" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
void handleRealTimeEvent(JSONObject response) throws IOException {
|
||||
String action = response.getString("action");
|
||||
JSONObject msg = response.has("data") ? response.getJSONObject("data") : new JSONObject();
|
||||
|
||||
String action = response.optString("action", "");
|
||||
JSONObject msg = response.has("data") ? response.optJSONObject("data") : new JSONObject();
|
||||
|
||||
switch (action) {
|
||||
case "added_to_group", "added_to_channel",
|
||||
"removed_from_group", "removed_from_channel", "chat_deleted","created_private_chat" -> {
|
||||
"removed_from_group", "removed_from_channel",
|
||||
"chat_deleted", "created_private_chat" -> {
|
||||
System.out.println("🔄 Chat list changed. Updating...");
|
||||
Session.forceRefreshChatList = true;
|
||||
System.out.println("🧪 Calling requestChatList() after being added");
|
||||
|
||||
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...");
|
||||
@@ -119,33 +148,22 @@ public class IncomingMessageListener implements Runnable {
|
||||
|
||||
case "chat_updated" -> {
|
||||
System.out.println("\n🔄 Chat info updated.");
|
||||
|
||||
if (msg.has("last_message_time")) {
|
||||
updateLastMessageTime(msg);
|
||||
updateLastMessageTime(msg);
|
||||
} else {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
handleAdminRoleChanged(msg);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try { handleAdminRoleChanged(msg); } catch (IOException e) { e.printStackTrace(); }
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
case "became_admin", "removed_admin", "ownership_transferred","admin_permissions_updated" -> {
|
||||
case "became_admin", "removed_admin", "ownership_transferred", "admin_permissions_updated" -> {
|
||||
System.out.println("🧩 Detected admin/owner role change. Calling handler...");
|
||||
new Thread(() -> {
|
||||
try {
|
||||
handleAdminRoleChanged(msg); //new thread
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try { handleAdminRoleChanged(msg); } catch (IOException e) { e.printStackTrace(); }
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
default -> displayRealTimeMessage(action, msg);
|
||||
}
|
||||
|
||||
@@ -157,25 +175,19 @@ public class IncomingMessageListener implements Runnable {
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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());
|
||||
@@ -185,24 +197,21 @@ public class IncomingMessageListener implements Runnable {
|
||||
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
|
||||
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()); // descending
|
||||
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()); // descending
|
||||
return c2.getLastMessageTime().compareTo(c1.getLastMessageTime());
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
if (Session.inChatListMenu) {
|
||||
ActionHandler.displayChatList();
|
||||
System.out.print("Select a chat by number: ");
|
||||
@@ -213,12 +222,12 @@ public class IncomingMessageListener implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -226,11 +235,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");
|
||||
@@ -256,21 +264,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...");
|
||||
@@ -293,20 +297,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");
|
||||
@@ -325,47 +320,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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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 {
|
||||
// 1) سوییچ مود با PrintWriter
|
||||
outText.print("MEDIA_DL\n");
|
||||
outText.flush();
|
||||
|
||||
// 2) هدر باینری درخواست
|
||||
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();
|
||||
|
||||
// 3) پاسخ
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import org.json.JSONObject;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
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";
|
||||
@@ -22,7 +24,10 @@ public class TelegramClient {
|
||||
public static BlockingQueue<JSONObject> responseQueue = new LinkedBlockingQueue<>();
|
||||
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;
|
||||
|
||||
@@ -31,6 +36,10 @@ public class TelegramClient {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static SocketMediaDownloader getDownloader() {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
public static TelegramClient getInstance() {
|
||||
return instance;
|
||||
}
|
||||
@@ -39,9 +48,14 @@ public class TelegramClient {
|
||||
public void start() {
|
||||
try {
|
||||
socket = new Socket(SERVER_HOST, SERVER_PORT);
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
out = new PrintWriter(socket.getOutputStream(), true);
|
||||
outBin = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); // برای MEDIA
|
||||
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);
|
||||
@@ -78,7 +92,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.");
|
||||
@@ -120,3 +134,4 @@ public class TelegramClient {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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,14 +1,12 @@
|
||||
package org.to.telegramfinalproject.Database;
|
||||
|
||||
import org.to.telegramfinalproject.Models.FileAttachment;
|
||||
import org.to.telegramfinalproject.Models.MediaRow;
|
||||
import org.to.telegramfinalproject.Models.Message;
|
||||
|
||||
import java.sql.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MessageDatabase {
|
||||
@@ -1197,4 +1195,171 @@ 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 ChannelDatabase.isUserInChannel(mr.receiverId, requester);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -315,4 +315,27 @@ public class PrivateChatDatabase {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
@@ -32,12 +32,31 @@ public class ClientHandler implements Runnable {
|
||||
UUID userId = null;
|
||||
|
||||
try (
|
||||
|
||||
// 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
|
||||
// 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);
|
||||
//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);
|
||||
|
||||
) {
|
||||
|
||||
@@ -45,11 +64,23 @@ public class ClientHandler implements Runnable {
|
||||
|
||||
String inputLine;
|
||||
while ((inputLine = readUtf8Line(bis)) != null) {
|
||||
|
||||
String line = inputLine.trim();
|
||||
|
||||
if ("MEDIA".equalsIgnoreCase(inputLine.trim())) {
|
||||
handleMediaFrame(dis, out);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("MEDIA_DL".equalsIgnoreCase(line)) {
|
||||
if (this.currentUser.getInternal_uuid() == null) {
|
||||
sendDlErr(dos, "not authorized");
|
||||
continue;
|
||||
}
|
||||
handleMediaDownload(dis, dos, this.currentUser.getInternal_uuid());
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject requestJson = new JSONObject(inputLine);
|
||||
String action = requestJson.getString("action");
|
||||
ResponseModel response = null;
|
||||
@@ -2144,6 +2175,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();
|
||||
@@ -2211,6 +2249,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);
|
||||
}
|
||||
|
||||
@@ -2570,7 +2628,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
|
||||
@@ -2863,6 +2923,75 @@ public class ClientHandler implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
Reference in New Issue
Block a user