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:
2025-09-02 17:15:17 +03:30
25 changed files with 2680 additions and 546 deletions
@@ -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.");