From 6431ce78464910304b3c3292d0073eb3719fd13f Mon Sep 17 00:00:00 2001 From: jarvis2f <137974272+jarvis2f@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:39:30 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Support=20preload=20file=20?= =?UTF-8?q?messages.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../telegram/files/AutoDownloadVerticle.java | 48 +- .../telegram/files/AutoRecordsHolder.java | 26 +- .../java/telegram/files/HttpVerticle.java | 31 +- .../main/java/telegram/files/MessyUtils.java | 47 ++ .../files/PreloadMessageVerticle.java | 120 +++++ .../main/java/telegram/files/TdApiHelp.java | 4 +- .../java/telegram/files/TelegramVerticle.java | 82 ++-- .../java/telegram/files/TransferVerticle.java | 10 +- .../files/repository/FileRepository.java | 2 + .../files/repository/SettingAutoRecords.java | 43 ++ .../repository/impl/FileRepositoryImpl.java | 18 +- .../telegram/files/AutoRecordsHolderTest.java | 2 +- web/src/components/auto-download-button.tsx | 17 +- web/src/components/auto-download-dialog.tsx | 444 +++++++++++------- web/src/components/file-type-filter.tsx | 10 +- web/src/components/telegram-icon.tsx | 2 +- web/src/lib/types.ts | 8 +- 17 files changed, 642 insertions(+), 272 deletions(-) create mode 100644 api/src/main/java/telegram/files/PreloadMessageVerticle.java diff --git a/api/src/main/java/telegram/files/AutoDownloadVerticle.java b/api/src/main/java/telegram/files/AutoDownloadVerticle.java index 6f7573e..e3ab525 100644 --- a/api/src/main/java/telegram/files/AutoDownloadVerticle.java +++ b/api/src/main/java/telegram/files/AutoDownloadVerticle.java @@ -8,7 +8,6 @@ import cn.hutool.log.LogFactory; import io.vertx.core.AbstractVerticle; import io.vertx.core.Future; import io.vertx.core.Promise; -import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import org.drinkless.tdlib.TdApi; import org.jooq.lambda.tuple.Tuple2; @@ -47,9 +46,9 @@ public class AutoDownloadVerticle extends AbstractVerticle { private int limit = DEFAULT_LIMIT; - public AutoDownloadVerticle(AutoRecordsHolder autoRecordsHolder) { - this.autoRecords = autoRecordsHolder.autoRecords(); - autoRecordsHolder.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> + public AutoDownloadVerticle() { + this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords(); + AutoRecordsHolder.INSTANCE.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> waitingDownloadMessages.getOrDefault(item.telegramId, new LinkedList<>()) .removeIf(m -> m.chatId == item.chatId))); } @@ -60,7 +59,10 @@ public class AutoDownloadVerticle extends AbstractVerticle { .compose(v -> this.initEventConsumer()) .onSuccess(v -> { vertx.setPeriodic(0, HISTORY_SCAN_INTERVAL, - id -> autoRecords.items.forEach(auto -> addHistoryMessage(auto, System.currentTimeMillis()))); + id -> autoRecords.getDownloadEnabledItems() + .stream() + .filter(auto -> auto.isNotComplete(SettingAutoRecords.HISTORY_DOWNLOAD_STATE)) + .forEach(auto -> addHistoryMessage(auto, System.currentTimeMillis()))); vertx.setPeriodic(0, DOWNLOAD_INTERVAL, id -> waitingDownloadMessages.keySet().forEach(this::download)); @@ -70,7 +72,7 @@ public class AutoDownloadVerticle extends AbstractVerticle { |Download interval: %s ms |Download limit: %s per telegram account! |Auto chats: %s - """.formatted(HISTORY_SCAN_INTERVAL, DOWNLOAD_INTERVAL, limit, autoRecords.items.size())); + """.formatted(HISTORY_SCAN_INTERVAL, DOWNLOAD_INTERVAL, limit, autoRecords.getDownloadEnabledItems().size())); startPromise.complete(); }) @@ -78,12 +80,8 @@ public class AutoDownloadVerticle extends AbstractVerticle { } @Override - public void stop(Promise stopPromise) { - saveAutoRecords() - .onComplete(r -> { - log.info("Auto download verticle stopped!"); - stopPromise.complete(); - }); + public void stop() { + log.info("Auto download verticle stopped!"); } private Future initAutoDownload() { @@ -109,19 +107,6 @@ public class AutoDownloadVerticle extends AbstractVerticle { return Future.succeededFuture(); } - private Future saveAutoRecords() { - return DataVerticle.settingRepository.getByKey(SettingKey.autoDownload) - .compose(settingAutoRecords -> { - if (settingAutoRecords == null) { - settingAutoRecords = new SettingAutoRecords(); - } - autoRecords.items.forEach(settingAutoRecords::add); - return DataVerticle.settingRepository.createOrUpdate(SettingKey.autoDownload.name(), Json.encode(settingAutoRecords)); - }) - .onFailure(e -> log.error("Save auto records failed!", e)) - .mapEmpty(); - } - private void addHistoryMessage(SettingAutoRecords.Item auto, long currentTimeMillis) { log.debug("Start scan history! TelegramId: %d ChatId: %d FileType: %s".formatted(auto.telegramId, auto.chatId, auto.nextFileType)); if (System.currentTimeMillis() - currentTimeMillis > MAX_HISTORY_SCAN_TIME) { @@ -158,12 +143,21 @@ public class AutoDownloadVerticle extends AbstractVerticle { addHistoryMessage(auto, currentTimeMillis); } else { log.debug("%s No more history files found! TelegramId: %d ChatId: %d".formatted(auto.uniqueKey(), auto.telegramId, auto.chatId)); + auto.complete(SettingAutoRecords.HISTORY_DOWNLOAD_STATE); } } else { DataVerticle.fileRepository.getFilesByUniqueId(TdApiHelp.getFileUniqueIds(Arrays.asList(foundChatMessages.messages))) .onSuccess(existFiles -> { List messages = Stream.of(foundChatMessages.messages) - .filter(message -> !existFiles.containsKey(TdApiHelp.getFileUniqueId(message))) + .filter(message -> { + String uniqueId = TdApiHelp.getFileUniqueId(message); + if (!existFiles.containsKey(uniqueId)) { + return true; + } else { + FileRecord fileRecord = existFiles.get(uniqueId); + return fileRecord.isDownloadStatus(FileRecord.DownloadStatus.idle); + } + }) .toList(); if (CollUtil.isEmpty(messages)) { auto.nextFromMessageId = foundChatMessages.nextFromMessageId; @@ -259,7 +253,7 @@ public class AutoDownloadVerticle extends AbstractVerticle { long telegramId = jsonObject.getLong("telegramId"); long chatId = jsonObject.getLong("chatId"); long messageId = jsonObject.getLong("messageId"); - autoRecords.items.stream() + autoRecords.getDownloadEnabledItems().stream() .filter(item -> item.telegramId == telegramId && item.chatId == chatId) .findFirst() .flatMap(item -> TelegramVerticles.get(telegramId)) diff --git a/api/src/main/java/telegram/files/AutoRecordsHolder.java b/api/src/main/java/telegram/files/AutoRecordsHolder.java index b89360f..2fcb50c 100644 --- a/api/src/main/java/telegram/files/AutoRecordsHolder.java +++ b/api/src/main/java/telegram/files/AutoRecordsHolder.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.log.Log; import cn.hutool.log.LogFactory; import io.vertx.core.Future; +import io.vertx.core.json.Json; import telegram.files.repository.SettingAutoRecords; import telegram.files.repository.SettingKey; @@ -18,7 +19,11 @@ public class AutoRecordsHolder { private final List>> onRemoveListeners = new ArrayList<>(); - public AutoRecordsHolder() { + private volatile boolean initialized = false; + + public static final AutoRecordsHolder INSTANCE = new AutoRecordsHolder(); + + private AutoRecordsHolder() { } public SettingAutoRecords autoRecords() { @@ -29,9 +34,13 @@ public class AutoRecordsHolder { onRemoveListeners.add(onRemove); } - public Future init() { + public synchronized Future init() { + if (initialized) { + return Future.succeededFuture(); + } return DataVerticle.settingRepository.getByKey(SettingKey.autoDownload) .onSuccess(settingAutoRecords -> { + initialized = true; if (settingAutoRecords == null) { return; } @@ -77,4 +86,17 @@ public class AutoRecordsHolder { onRemoveListeners.forEach(listener -> listener.accept(removedItems)); } } + + public Future saveAutoRecords() { + return DataVerticle.settingRepository.getByKey(SettingKey.autoDownload) + .compose(settingAutoRecords -> { + if (settingAutoRecords == null) { + settingAutoRecords = new SettingAutoRecords(); + } + autoRecords.items.forEach(settingAutoRecords::add); + return DataVerticle.settingRepository.createOrUpdate(SettingKey.autoDownload.name(), Json.encode(settingAutoRecords)); + }) + .onFailure(e -> log.error("Save auto records failed!", e)) + .mapEmpty(); + } } diff --git a/api/src/main/java/telegram/files/HttpVerticle.java b/api/src/main/java/telegram/files/HttpVerticle.java index 309cf31..977fcac 100644 --- a/api/src/main/java/telegram/files/HttpVerticle.java +++ b/api/src/main/java/telegram/files/HttpVerticle.java @@ -44,8 +44,6 @@ public class HttpVerticle extends AbstractVerticle { // session id -> telegram verticle private final Map sessionTelegramVerticles = new ConcurrentHashMap<>(); - private final AutoRecordsHolder autoRecordsHolder = new AutoRecordsHolder(); - private final FileRouteHandler fileRouteHandler = new FileRouteHandler(); private static final String SESSION_COOKIE_NAME = "tf"; @@ -54,17 +52,22 @@ public class HttpVerticle extends AbstractVerticle { public void start(Promise startPromise) { initHttpServer() .compose(r -> initTelegramVerticles()) - .compose(r -> autoRecordsHolder.init()) + .compose(r -> AutoRecordsHolder.INSTANCE.init()) .compose(r -> initAutoDownloadVerticle()) .compose(r -> initTransferVerticle()) + .compose(r -> initPreloadMessageVerticle()) .compose(r -> initEventConsumer()) .onSuccess(startPromise::complete) .onFailure(startPromise::fail); } @Override - public void stop() { - log.info("Http verticle stopped!"); + public void stop(Promise stopPromise) { + AutoRecordsHolder.INSTANCE.saveAutoRecords() + .onComplete(ignore -> { + log.info("Http verticle stopped!"); + stopPromise.complete(); + }); } public Future initHttpServer() { @@ -153,7 +156,7 @@ public class HttpVerticle extends AbstractVerticle { router.post("/:telegramId/file/cancel-download").handler(this::handleFileCancelDownload); router.post("/:telegramId/file/toggle-pause-download").handler(this::handleFileTogglePauseDownload); router.post("/:telegramId/file/remove").handler(this::handleFileRemove); - router.post("/:telegramId/file/auto-download").handler(this::handleAutoDownload); + router.post("/:telegramId/file/update-auto-settings").handler(this::handleAutoSettingsUpdate); router.route() .failureHandler(ctx -> { @@ -182,15 +185,19 @@ public class HttpVerticle extends AbstractVerticle { } public Future initAutoDownloadVerticle() { - return vertx.deployVerticle(new AutoDownloadVerticle(autoRecordsHolder), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS) + return vertx.deployVerticle(new AutoDownloadVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS) .mapEmpty(); } public Future initTransferVerticle() { - return vertx.deployVerticle(new TransferVerticle(autoRecordsHolder), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS) + return vertx.deployVerticle(new TransferVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS) .mapEmpty(); } + public Future initPreloadMessageVerticle() { + return vertx.deployVerticle(new PreloadMessageVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS) + .mapEmpty(); + } private Future initEventConsumer() { vertx.eventBus().consumer(EventEnum.TELEGRAM_EVENT.address(), message -> { @@ -211,8 +218,8 @@ public class HttpVerticle extends AbstractVerticle { }); vertx.eventBus().consumer(EventEnum.AUTO_DOWNLOAD_UPDATE.address(), message -> { - log.debug("Auto download update: %s".formatted(message.body())); - autoRecordsHolder.onAutoRecordsUpdate(Json.decodeValue(message.body().toString(), SettingAutoRecords.class)); + log.debug("Auto settings update: %s".formatted(message.body())); + AutoRecordsHolder.INSTANCE.onAutoRecordsUpdate(Json.decodeValue(message.body().toString(), SettingAutoRecords.class)); }); return Future.succeededFuture(); } @@ -607,7 +614,7 @@ public class HttpVerticle extends AbstractVerticle { .onFailure(ctx::fail); } - private void handleAutoDownload(RoutingContext ctx) { + private void handleAutoSettingsUpdate(RoutingContext ctx) { TelegramVerticle telegramVerticle = TelegramVerticles.getOrElseThrow(ctx.pathParam("telegramId")); String chatId = ctx.request().getParam("chatId"); @@ -616,7 +623,7 @@ public class HttpVerticle extends AbstractVerticle { return; } JsonObject params = ctx.body().asJsonObject(); - telegramVerticle.toggleAutoDownload(Convert.toLong(chatId), params) + telegramVerticle.updateAutoSettings(Convert.toLong(chatId), params) .onSuccess(r -> ctx.end()) .onFailure(ctx::fail); } diff --git a/api/src/main/java/telegram/files/MessyUtils.java b/api/src/main/java/telegram/files/MessyUtils.java index 16dcc41..80ed2db 100644 --- a/api/src/main/java/telegram/files/MessyUtils.java +++ b/api/src/main/java/telegram/files/MessyUtils.java @@ -85,4 +85,51 @@ public class MessyUtils { default -> throw new IllegalArgumentException("Unknown unit: " + unit); }; } + + public static class BitState { + private int state; + + public BitState(int state) { + this.state = state; + } + + /** + * 开启某个状态 + */ + public void enableState(int n) { + state |= (1 << n); + } + + /** + * 关闭某个状态 + */ + public void disableState(int n) { + state &= ~(1 << n); + } + + /** + * 切换某个状态(开启->关闭,关闭->开启) + */ + public void toggleState(int n) { + state ^= (1 << n); + } + + /** + * 检查某个状态是否开启 + */ + public boolean isStateEnabled(int n) { + return (state & (1 << n)) != 0; + } + + public int getState() { + return state; + } + + /** + * 获取当前状态的二进制表示 + */ + public String getBinaryState() { + return String.format("%8s", Integer.toBinaryString(state)).replace(' ', '0'); + } + } } diff --git a/api/src/main/java/telegram/files/PreloadMessageVerticle.java b/api/src/main/java/telegram/files/PreloadMessageVerticle.java new file mode 100644 index 0000000..321ac4d --- /dev/null +++ b/api/src/main/java/telegram/files/PreloadMessageVerticle.java @@ -0,0 +1,120 @@ +package telegram.files; + +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.json.JsonObject; +import org.drinkless.tdlib.TdApi; +import telegram.files.repository.FileRecord; +import telegram.files.repository.SettingAutoRecords; + +import java.util.Optional; + +public class PreloadMessageVerticle extends AbstractVerticle { + + private static final Log log = LogFactory.get(); + + private static final int HISTORY_SCAN_INTERVAL = 30 * 1000; + + private static final int MAX_HISTORY_SCAN_TIME = 10 * 1000; + + private final SettingAutoRecords autoRecords; + + public PreloadMessageVerticle() { + this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords(); + } + + @Override + public void start(Promise startPromise) { + initEventConsumer() + .onSuccess(r -> { + vertx.setPeriodic(0, HISTORY_SCAN_INTERVAL, + id -> autoRecords.getPreloadEnabledItems() + .stream() + .filter(auto -> auto.isNotComplete(SettingAutoRecords.HISTORY_PRELOAD_STATE)) + .forEach(auto -> addHistoryMessage(auto, System.currentTimeMillis()))); + + log.info(""" + Preload message verticle started! + |Auto chats: %s + """.formatted(autoRecords.getPreloadEnabledItems().size())); + + startPromise.complete(); + }) + .onFailure(startPromise::fail); + } + + @Override + public void stop() { + log.info("Preload message verticle stopped!"); + } + + private Future initEventConsumer() { + vertx.eventBus().consumer(EventEnum.MESSAGE_RECEIVED.address(), message -> { + log.trace("Auto download message received: %s".formatted(message.body())); + this.onNewMessage((JsonObject) message.body()); + }); + return Future.succeededFuture(); + } + + private void addHistoryMessage(SettingAutoRecords.Item auto, long currentTimeMillis) { + log.debug("Start load history message! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId)); + if (System.currentTimeMillis() - currentTimeMillis > MAX_HISTORY_SCAN_TIME) { + log.debug("Load history message timeout! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId)); + return; + } + + TelegramVerticle telegramVerticle = TelegramVerticles.getOrElseThrow(auto.telegramId); + TdApi.SearchChatMessages searchChatMessages = new TdApi.SearchChatMessages(); + searchChatMessages.chatId = auto.chatId; + searchChatMessages.fromMessageId = auto.nextFromMessageIdForPreload; + searchChatMessages.limit = 100; + TdApi.FoundChatMessages foundChatMessages = Future.await(telegramVerticle.client.execute(searchChatMessages) + .onFailure(r -> log.error("Search chat messages failed! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId), r)) + ); + if (foundChatMessages == null || foundChatMessages.messages.length == 0) { + log.debug("%s No more history message found! TelegramId: %d ChatId: %d".formatted(auto.uniqueKey(), auto.telegramId, auto.chatId)); + auto.complete(SettingAutoRecords.HISTORY_PRELOAD_STATE); + return; + } + int count = 0; + for (TdApi.Message message : foundChatMessages.messages) { + Optional> fileHandlerOptional = TdApiHelp.getFileHandler(message); + if (fileHandlerOptional.isEmpty()) { + continue; + } + FileRecord fileRecord = fileHandlerOptional.get().convertFileRecord(auto.telegramId); + if (Future.await(DataVerticle.fileRepository.createIfNotExist(fileRecord))) { + count++; + } + } + + if (log.isDebugEnabled() && count > 0) { + log.debug("Load history message success! TelegramId: %d ChatId: %d Count: %d".formatted(auto.telegramId, auto.chatId, count)); + } + auto.nextFromMessageIdForPreload = foundChatMessages.nextFromMessageId; + addHistoryMessage(auto, currentTimeMillis); + } + + private void onNewMessage(JsonObject jsonObject) { + long telegramId = jsonObject.getLong("telegramId"); + long chatId = jsonObject.getLong("chatId"); + long messageId = jsonObject.getLong("messageId"); + autoRecords.getPreloadEnabledItems().stream() + .filter(item -> item.telegramId == telegramId && item.chatId == chatId) + .findFirst() + .flatMap(ignore -> TelegramVerticles.get(telegramId)) + .ifPresent(telegramVerticle -> { + if (!telegramVerticle.authorized) return; + + telegramVerticle.client.execute(new TdApi.GetMessage(chatId, messageId)) + .onSuccess(message -> TdApiHelp.getFileHandler(message).ifPresent(fileHandler -> { + FileRecord fileRecord = fileHandler.convertFileRecord(telegramId); + DataVerticle.fileRepository.createIfNotExist(fileRecord); + })) + .onFailure(e -> log.error("Preload message fail. Get message failed: %s".formatted(e.getMessage()))); + }); + } +} diff --git a/api/src/main/java/telegram/files/TdApiHelp.java b/api/src/main/java/telegram/files/TdApiHelp.java index ec26f4a..bebedfc 100644 --- a/api/src/main/java/telegram/files/TdApiHelp.java +++ b/api/src/main/java/telegram/files/TdApiHelp.java @@ -193,7 +193,9 @@ public class TdApiHelp { case TdApi.MessageDocument.CONSTRUCTOR -> { return Optional.of((T) new DocumentHandler(message)); } - default -> throw new NoStackTraceException("Unsupported message type: " + message.content.getConstructor()); + default -> { + return Optional.empty(); + } } } diff --git a/api/src/main/java/telegram/files/TelegramVerticle.java b/api/src/main/java/telegram/files/TelegramVerticle.java index 2b21fd2..27d87a3 100644 --- a/api/src/main/java/telegram/files/TelegramVerticle.java +++ b/api/src/main/java/telegram/files/TelegramVerticle.java @@ -312,11 +312,15 @@ public class TelegramVerticle extends AbstractVerticle { TdApiHelp.FileHandler fileHandler = TdApiHelp.getFileHandler(message) .orElseThrow(() -> new NoStackTraceException("not support message type")); FileRecord fileRecord = fileHandler.convertFileRecord(telegramRecord.id()); - return DataVerticle.fileRepository.create(fileRecord) - .compose(r -> - client.execute(new TdApi.AddFileToDownloads(fileId, chatId, messageId, 32)) - ) - .onSuccess(r -> + return DataVerticle.fileRepository.createIfNotExist(fileRecord) + .compose(r -> { + if (!r) { + return DataVerticle.fileRepository.updateFileId(fileRecord.id(), fileRecord.uniqueId()); + } + return Future.succeededFuture(); + }) + .compose(ignore -> client.execute(new TdApi.AddFileToDownloads(fileId, chatId, messageId, 32))) + .onSuccess(ignore -> sendEvent(EventPayload.build(EventPayload.TYPE_FILE_STATUS, new JsonObject() .put("fileId", fileId) .put("uniqueId", fileRecord.uniqueId()) @@ -430,24 +434,33 @@ public class TelegramVerticle extends AbstractVerticle { .mapEmpty(); } - public Future toggleAutoDownload(Long chatId, JsonObject params) { + public Future updateAutoSettings(Long chatId, JsonObject params) { return DataVerticle.settingRepository.getByKey(SettingKey.autoDownload) .compose(settingAutoRecords -> { if (settingAutoRecords == null) { settingAutoRecords = new SettingAutoRecords(); } - if (settingAutoRecords.exists(this.telegramRecord.id(), chatId)) { - settingAutoRecords.remove(this.telegramRecord.id(), chatId); - } else { - SettingAutoRecords.Rule rule = null; - if (params != null && params.containsKey("rule")) { - rule = params.getJsonObject("rule").mapTo(SettingAutoRecords.Rule.class); - if (StrUtil.isBlank(rule.query) && CollUtil.isEmpty(rule.fileTypes) && rule.transferRule == null) { - rule = null; - } - } - settingAutoRecords.add(this.telegramRecord.id(), chatId, rule); + SettingAutoRecords.Rule rule = params.getJsonObject("rule").mapTo(SettingAutoRecords.Rule.class); + if (StrUtil.isBlank(rule.query) && CollUtil.isEmpty(rule.fileTypes) && rule.transferRule == null) { + rule = null; } + boolean downloadEnabled = params.getBoolean("downloadEnabled", false); + boolean preloadEnabled = params.getBoolean("preloadEnabled", false); + + SettingAutoRecords.Item item = settingAutoRecords.getItem(this.telegramRecord.id(), chatId); + if (downloadEnabled || preloadEnabled) { + if (item == null) { + item = new SettingAutoRecords.Item(this.telegramRecord.id(), chatId, rule); + } + item.downloadEnabled = downloadEnabled; + item.preloadEnabled = preloadEnabled; + item.rule = downloadEnabled ? rule : null; + + settingAutoRecords.add(item); + } else if (item != null) { + settingAutoRecords.remove(this.telegramRecord.id(), chatId); + } + return DataVerticle.settingRepository.createOrUpdate(SettingKey.autoDownload.name(), Json.encode(settingAutoRecords)); }) .onSuccess(r -> vertx.eventBus().publish(EventEnum.AUTO_DOWNLOAD_UPDATE.name(), r.value())) @@ -807,25 +820,22 @@ public class TelegramVerticle extends AbstractVerticle { //<-----------------------------convert----------------------------------> private Future convertChat(List chats) { - return DataVerticle.settingRepository.getByKey(SettingKey.autoDownload) - .map(settingAutoRecords -> settingAutoRecords == null ? new HashMap() - : settingAutoRecords.getItems(this.telegramRecord.id())) - .map(enableAutoChats -> new JsonArray(chats.stream() - .map(chat -> { - SettingAutoRecords.Item autoItem = enableAutoChats.get(chat.id); - return new JsonObject() - .put("id", Convert.toStr(chat.id)) - .put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title) - .put("type", TdApiHelp.getChatType(chat.type)) - .put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(chat, "photo.minithumbnail.data"))) - .put("unreadCount", chat.unreadCount) - .put("lastMessage", "") - .put("lastMessageTime", "") - .put("autoEnabled", autoItem != null) - .put("autoRule", autoItem == null ? null : autoItem.rule); - }) - .toList() - )); + Map enableAutoChats = AutoRecordsHolder.INSTANCE.autoRecords().getItems(this.telegramRecord.id()); + return Future.succeededFuture(new JsonArray(chats.stream() + .map(chat -> { + SettingAutoRecords.Item auto = enableAutoChats.get(chat.id); + return new JsonObject() + .put("id", Convert.toStr(chat.id)) + .put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title) + .put("type", TdApiHelp.getChatType(chat.type)) + .put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(chat, "photo.minithumbnail.data"))) + .put("unreadCount", chat.unreadCount) + .put("lastMessage", "") + .put("lastMessageTime", "") + .put("auto", auto); + }) + .toList() + )); } private Future convertFiles(Tuple2> tuple) { diff --git a/api/src/main/java/telegram/files/TransferVerticle.java b/api/src/main/java/telegram/files/TransferVerticle.java index 8cd4866..8963580 100644 --- a/api/src/main/java/telegram/files/TransferVerticle.java +++ b/api/src/main/java/telegram/files/TransferVerticle.java @@ -38,9 +38,9 @@ public class TransferVerticle extends AbstractVerticle { private volatile Transfer beingTransferred; - public TransferVerticle(AutoRecordsHolder autoRecordsHolder) { - this.autoRecords = autoRecordsHolder.autoRecords(); - autoRecordsHolder.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> { + public TransferVerticle() { + this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords(); + AutoRecordsHolder.INSTANCE.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> { waitingTransferFiles.removeIf(waitingTransferFile -> waitingTransferFile.uniqueId().equals(item.uniqueKey())); transfers.remove(item.uniqueKey()); })); @@ -109,7 +109,7 @@ public class TransferVerticle extends AbstractVerticle { log.debug("Start scan history files for transfer"); for (SettingAutoRecords.Item item : autoRecords.items) { Transfer transfer = getTransfer(item); - if (transfer == null || !transfer.transferHistory) { + if (transfer == null || !transfer.transferHistory || item.isNotComplete(SettingAutoRecords.HISTORY_PRELOAD_STATE)) { continue; } Tuple3, Long, Long> filesTuple = Future.await(DataVerticle.fileRepository.getFiles(item.chatId, @@ -119,6 +119,8 @@ public class TransferVerticle extends AbstractVerticle { )); List files = filesTuple.v1; if (CollUtil.isEmpty(files)) { + log.debug("No history files found for transfer: %s".formatted(item.uniqueKey())); + item.complete(SettingAutoRecords.HISTORY_TRANSFER_STATE); continue; } diff --git a/api/src/main/java/telegram/files/repository/FileRepository.java b/api/src/main/java/telegram/files/repository/FileRepository.java index 0d378e4..78a855f 100644 --- a/api/src/main/java/telegram/files/repository/FileRepository.java +++ b/api/src/main/java/telegram/files/repository/FileRepository.java @@ -11,6 +11,8 @@ import java.util.Map; public interface FileRepository { Future create(FileRecord fileRecord); + Future createIfNotExist(FileRecord fileRecord); + Future> getFiles(long chatId, List fileIds); Future, Long, Long>> getFiles(long chatId, Map filter); diff --git a/api/src/main/java/telegram/files/repository/SettingAutoRecords.java b/api/src/main/java/telegram/files/repository/SettingAutoRecords.java index 74ff165..07fbb11 100644 --- a/api/src/main/java/telegram/files/repository/SettingAutoRecords.java +++ b/api/src/main/java/telegram/files/repository/SettingAutoRecords.java @@ -1,5 +1,7 @@ package telegram.files.repository; +import com.fasterxml.jackson.annotation.JsonIgnore; +import telegram.files.MessyUtils; import telegram.files.Transfer; import java.util.ArrayList; @@ -12,6 +14,12 @@ public class SettingAutoRecords { public List items; + public static final int HISTORY_PRELOAD_STATE = 1; + + public static final int HISTORY_DOWNLOAD_STATE = 2; + + public static final int HISTORY_TRANSFER_STATE = 3; + public static class Item { public long telegramId; @@ -23,7 +31,17 @@ public class SettingAutoRecords { public Rule rule; + public boolean downloadEnabled; + + public boolean preloadEnabled; + + public long nextFromMessageIdForPreload; + + public int state; + public Item() { + // downloadEnabled default is true + this.downloadEnabled = true; } public Item(long telegramId, long chatId, Rule rule) { @@ -35,6 +53,17 @@ public class SettingAutoRecords { public String uniqueKey() { return telegramId + ":" + chatId; } + + public void complete(int bitwise) { + MessyUtils.BitState bitState = new MessyUtils.BitState(state); + bitState.enableState(bitwise); + state = bitState.getState(); + } + + public boolean isNotComplete(int bitwise) { + MessyUtils.BitState bitState = new MessyUtils.BitState(state); + return !bitState.isStateEnabled(bitwise); + } } public static class Rule { @@ -102,6 +131,20 @@ public class SettingAutoRecords { items.removeIf(item -> item.telegramId == telegramId && item.chatId == chatId); } + @JsonIgnore + public List getDownloadEnabledItems() { + return items.stream() + .filter(i -> i.downloadEnabled) + .toList(); + } + + @JsonIgnore + public List getPreloadEnabledItems() { + return items.stream() + .filter(i -> i.preloadEnabled) + .toList(); + } + public Map getItems(long telegramId) { return items.stream() .filter(item -> item.telegramId == telegramId) diff --git a/api/src/main/java/telegram/files/repository/impl/FileRepositoryImpl.java b/api/src/main/java/telegram/files/repository/impl/FileRepositoryImpl.java index 533859a..5ac5537 100644 --- a/api/src/main/java/telegram/files/repository/impl/FileRepositoryImpl.java +++ b/api/src/main/java/telegram/files/repository/impl/FileRepositoryImpl.java @@ -57,11 +57,19 @@ public class FileRepositoryImpl implements FileRepository { .execute(fileRecord) .map(r -> fileRecord) .compose(r -> this.updateCaptionByMediaAlbumId(fileRecord.mediaAlbumId(), fileRecord.caption()).map(r)) - .onSuccess(r -> log.trace("Successfully created file record: %s".formatted(fileRecord.id())) - ) - .onFailure( - err -> log.error("Failed to create file record: %s".formatted(err.getMessage())) - ); + .onSuccess(r -> log.trace("Successfully created file record: %s".formatted(fileRecord.id()))) + .onFailure(err -> log.error("Failed to create file record: %s".formatted(err.getMessage()))); + } + + @Override + public Future createIfNotExist(FileRecord fileRecord) { + return this.getByUniqueId(fileRecord.uniqueId()) + .compose(record -> { + if (record != null) { + return Future.succeededFuture(false); + } + return this.create(fileRecord).map(true); + }); } @Override diff --git a/api/src/test/java/telegram/files/AutoRecordsHolderTest.java b/api/src/test/java/telegram/files/AutoRecordsHolderTest.java index 8364b15..c3d3f80 100644 --- a/api/src/test/java/telegram/files/AutoRecordsHolderTest.java +++ b/api/src/test/java/telegram/files/AutoRecordsHolderTest.java @@ -28,7 +28,7 @@ public class AutoRecordsHolderTest { @BeforeEach public void setUp() { - autoRecordsHolder = new AutoRecordsHolder(); + autoRecordsHolder = AutoRecordsHolder.INSTANCE; telegramVerticlesMockedStatic = mockStatic(TelegramVerticles.class); // Prepare test data diff --git a/web/src/components/auto-download-button.tsx b/web/src/components/auto-download-button.tsx index bb978e7..3452c52 100644 --- a/web/src/components/auto-download-button.tsx +++ b/web/src/components/auto-download-button.tsx @@ -8,16 +8,19 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { type TelegramChat } from "@/lib/types"; interface AutoDownloadButtonProps extends React.ButtonHTMLAttributes { - autoEnabled?: boolean; + auto?: TelegramChat["auto"]; } const AutoDownloadButton = React.forwardRef< HTMLButtonElement, AutoDownloadButtonProps ->(({ autoEnabled = false, className, ...props }, ref) => { +>(({ auto, className, ...props }, ref) => { + const autoEnabled = auto && (auto.downloadEnabled || auto.preloadEnabled); + return ( @@ -40,15 +43,19 @@ const AutoDownloadButton = React.forwardRef<
{autoEnabled - ? "Auto download is enabled, you can disable it by clicking the button" - : "Auto download is disabled, you can enable it by clicking the button"} + ? "Automation is enabled, you can disable it by clicking the button" + : "Automation is disabled, you can enable it by clicking the button"}
diff --git a/web/src/components/auto-download-dialog.tsx b/web/src/components/auto-download-dialog.tsx index 33a6d88..db8d9ef 100644 --- a/web/src/components/auto-download-dialog.tsx +++ b/web/src/components/auto-download-dialog.tsx @@ -8,7 +8,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import useSWRMutation from "swr/mutation"; import { POST } from "@/lib/api"; import { useDebounce } from "use-debounce"; @@ -55,6 +55,9 @@ export default function AutoDownloadDialog() { const { isLoading, chat, reload } = useTelegramChat(); const { toast } = useToast(); const [open, setOpen] = useState(false); + const [editMode, setEditMode] = useState(false); + const [downloadEnabled, setDownloadEnabled] = useState(false); + const [preloadEnabled, setPreloadEnabled] = useState(false); const [rule, setRule] = useState({ query: "", fileTypes: [], @@ -62,18 +65,28 @@ export default function AutoDownloadDialog() { const { trigger: triggerAuto, isMutating: isAutoMutating } = useSWRMutation( !accountId || !chat ? undefined - : `/${accountId}/file/auto-download?telegramId=${accountId}&chatId=${chat?.id}`, - (key, { arg }: { arg: { rule: AutoDownloadRule } }) => { + : `/${accountId}/file/update-auto-settings?telegramId=${accountId}&chatId=${chat?.id}`, + ( + key, + { + arg, + }: { + arg: { + downloadEnabled: boolean; + preloadEnabled: boolean; + rule: AutoDownloadRule; + }; + }, + ) => { return POST(key, arg); }, { onSuccess: () => { toast({ - title: chat?.autoEnabled - ? "Auto download disabled for this chat!" - : "Auto download enabled for this chat!", + title: "Auto settings updated!", }); void reload(); + setEditMode(false); setTimeout(() => { setOpen(false); }, 1000); @@ -85,6 +98,18 @@ export default function AutoDownloadDialog() { leading: true, }); + useEffect(() => { + if (chat?.auto) { + setDownloadEnabled(chat.auto.downloadEnabled); + setPreloadEnabled(chat.auto.preloadEnabled); + setRule(chat.auto.rule ?? { query: "", fileTypes: [] }); + } else { + setDownloadEnabled(false); + setPreloadEnabled(false); + setRule({ query: "", fileTypes: [] }); + } + }, [chat]); + if (isLoading) { return (
@@ -100,202 +125,269 @@ export default function AutoDownloadDialog() { setOpen(!open); }} > - {chat && } + {chat && } setOpen(false)} onClick={(e) => e.stopPropagation()} - className="h-full w-full overflow-auto md:h-auto md:max-h-[85%] md:w-auto" + className="h-full w-full overflow-auto md:h-auto md:max-h-[85%] md:min-w-[400px]" > - {chat?.autoEnabled - ? "Disable Auto Download" - : "Enable auto download for this chat?"} + Update Auto Settings for {chat?.name ?? "Unknown Chat"} - {chat?.autoEnabled ? ( + {!editMode && chat?.auto ? (
- {chat.autoRule && ( -
-
- - - Active - -
- -
- {/* Filter Keyword Section */} -
-
- - Filter Keyword - - - {chat.autoRule.query || "No keyword specified"} - -
-
- -
- - File Types - -
- {chat.autoRule.fileTypes.length > 0 ? ( - chat.autoRule.fileTypes.map((type) => ( - - {type} - - )) - ) : ( - - No file types selected - - )} -
-
- - {/* Transfer Rule Section */} - {chat.autoRule.transferRule && ( +
+
+ + + {chat.auto.preloadEnabled ? "Enabled" : "Disabled"} + +
+ {(chat.auto.state & (1 << 1)) != 0 && ( +

+ All historical files are preloaded. +

+ )} +
+
+
+ + + {chat.auto.downloadEnabled ? "Enabled" : "Disabled"} + +
+ {(chat.auto.state & (1 << 2)) != 0 && ( +

+ All historical files are downloaded. +

+ )} + {chat.auto.rule && ( +
+
+ {/* Filter Keyword Section */}
-
+
- Transfer Rule + Filter Keyword -
-
- - Destination Folder - - - {chat.autoRule.transferRule.destination} - -
-
- - Transfer Policy - - - {chat.autoRule.transferRule.transferPolicy} - -
-
- - Duplication Policy - - - {chat.autoRule.transferRule.duplicationPolicy} - -
-
- - Transfer History - - - {chat.autoRule.transferRule.transferHistory - ? "Enabled" - : "Disabled"} + + {chat.auto.rule.query || "No keyword specified"} + +
+
+ +
+ + File Types + +
+ {chat.auto.rule.fileTypes.length > 0 ? ( + chat.auto.rule.fileTypes.map((type) => ( + + {type} + )) + ) : ( + + No file types selected + + )} +
+
+ + {/* Transfer Rule Section */} + {chat.auto.rule.transferRule && ( +
+
+ + Transfer Rule + +
+
+ + Destination Folder + + + {chat.auto.rule.transferRule.destination} + +
+
+ + Transfer Policy + + + {chat.auto.rule.transferRule.transferPolicy} + +
+
+ + Duplication Policy + + + {chat.auto.rule.transferRule.duplicationPolicy} + +
+
+ + Transfer History + + + {chat.auto.rule.transferRule.transferHistory + ? "Enabled" + : "Disabled"} + +
-
- )} + )} +
-
- )} -
-
- -

- This will disable auto download for this chat. You can always - enable it later. -

-
-
- -

- Files that are being downloaded will be paused and you can - enable automatic downloading again later. -

-
+ )}
) : (
-
-
- -

- This will enable auto download for this chat. Files will be - downloaded automatically. -

-
-
- -

- Files in historical messages will be downloaded first, and - then files in new messages will be downloaded automatically. -

-
-
- -

- Download Order: - - {"Photo -> Video -> Audio -> File"} - -

+
+
+ +
+ {preloadEnabled && ( +
+
+ +

+ This will enable preload for this chat. All files will be + loaded, but not downloaded, then you can search offline. +

+
+
+ )} +
+
+
+ + { + if (checked) { + setRule({ + query: "", + fileTypes: [], + }); + } + setDownloadEnabled(checked); + }} + /> +
+ {downloadEnabled && ( + <> +
+
+ +

+ This will enable auto download for this chat. Files will + be downloaded automatically. +

+
+
+ +

+ Files in historical messages will be downloaded first, + and then files in new messages will be downloaded + automatically. +

+
+
+ +

+ Download Order: + + {"Photo -> Video -> Audio -> File"} + +

+
+
+ + + )}
-
)} - - + {!editMode && chat?.auto ? ( + + ) : ( + <> + + + + )} @@ -546,11 +638,13 @@ const PolicyLegends: Record< }, OVERWRITE: { title: "Overwrite", - description: "If destination exists same name file, move and overwrite the file.", + description: + "If destination exists same name file, move and overwrite the file.", }, SKIP: { title: "Skip", - description: "If destination exists same name file, skip the file, nothing to do.", + description: + "If destination exists same name file, skip the file, nothing to do.", }, RENAME: { title: "Rename", diff --git a/web/src/components/file-type-filter.tsx b/web/src/components/file-type-filter.tsx index d0e214b..2f66385 100644 --- a/web/src/components/file-type-filter.tsx +++ b/web/src/components/file-type-filter.tsx @@ -10,6 +10,7 @@ import useSWR from "swr"; import { Ellipsis } from "lucide-react"; import { Label } from "@/components/ui/label"; import * as React from "react"; +import { useEffect } from "react"; interface FileTypeFilterProps { offline: boolean; @@ -55,6 +56,13 @@ export default function FileTypeFilter({ ); }; + useEffect(() => { + if (!offline && localType === "all") { + setLocalType("media"); + onChange("media"); + } + }, [localType, offline, onChange]); + return (
@@ -63,7 +71,7 @@ export default function FileTypeFilter({ - {offline && All files} + {offline && All Files} diff --git a/web/src/components/telegram-icon.tsx b/web/src/components/telegram-icon.tsx index 9f2c26b..2d7c985 100644 --- a/web/src/components/telegram-icon.tsx +++ b/web/src/components/telegram-icon.tsx @@ -6,7 +6,7 @@ export default function TelegramIcon({ className }: { className?: string }) { role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" - className={cn(className, "dark:fill-current")} + className={cn(className, "dark:fill-white")} > Telegram diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 805719b..19f2c1d 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -19,8 +19,12 @@ export type TelegramChat = { unreadCount?: number; lastMessage?: string; lastMessageTime?: string; - autoEnabled: boolean; - autoRule?: AutoDownloadRule; + auto?: { + preloadEnabled: boolean; + downloadEnabled: boolean; + rule?: AutoDownloadRule; + state: number; + }; }; export type FileType = "media" | "photo" | "video" | "audio" | "file";