feat: Support preload file messages.

This commit is contained in:
jarvis2f 2025-03-05 16:39:30 +08:00
parent 23fd65b7f0
commit 6431ce7846
17 changed files with 642 additions and 272 deletions

View file

@ -8,7 +8,6 @@ import cn.hutool.log.LogFactory;
import io.vertx.core.AbstractVerticle; import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import org.drinkless.tdlib.TdApi; import org.drinkless.tdlib.TdApi;
import org.jooq.lambda.tuple.Tuple2; import org.jooq.lambda.tuple.Tuple2;
@ -47,9 +46,9 @@ public class AutoDownloadVerticle extends AbstractVerticle {
private int limit = DEFAULT_LIMIT; private int limit = DEFAULT_LIMIT;
public AutoDownloadVerticle(AutoRecordsHolder autoRecordsHolder) { public AutoDownloadVerticle() {
this.autoRecords = autoRecordsHolder.autoRecords(); this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords();
autoRecordsHolder.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> AutoRecordsHolder.INSTANCE.registerOnRemoveListener(removedItems -> removedItems.forEach(item ->
waitingDownloadMessages.getOrDefault(item.telegramId, new LinkedList<>()) waitingDownloadMessages.getOrDefault(item.telegramId, new LinkedList<>())
.removeIf(m -> m.chatId == item.chatId))); .removeIf(m -> m.chatId == item.chatId)));
} }
@ -60,7 +59,10 @@ public class AutoDownloadVerticle extends AbstractVerticle {
.compose(v -> this.initEventConsumer()) .compose(v -> this.initEventConsumer())
.onSuccess(v -> { .onSuccess(v -> {
vertx.setPeriodic(0, HISTORY_SCAN_INTERVAL, 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, vertx.setPeriodic(0, DOWNLOAD_INTERVAL,
id -> waitingDownloadMessages.keySet().forEach(this::download)); id -> waitingDownloadMessages.keySet().forEach(this::download));
@ -70,7 +72,7 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|Download interval: %s ms |Download interval: %s ms
|Download limit: %s per telegram account! |Download limit: %s per telegram account!
|Auto chats: %s |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(); startPromise.complete();
}) })
@ -78,12 +80,8 @@ public class AutoDownloadVerticle extends AbstractVerticle {
} }
@Override @Override
public void stop(Promise<Void> stopPromise) { public void stop() {
saveAutoRecords()
.onComplete(r -> {
log.info("Auto download verticle stopped!"); log.info("Auto download verticle stopped!");
stopPromise.complete();
});
} }
private Future<Void> initAutoDownload() { private Future<Void> initAutoDownload() {
@ -109,19 +107,6 @@ public class AutoDownloadVerticle extends AbstractVerticle {
return Future.succeededFuture(); return Future.succeededFuture();
} }
private Future<Void> saveAutoRecords() {
return DataVerticle.settingRepository.<SettingAutoRecords>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) { 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)); 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) { if (System.currentTimeMillis() - currentTimeMillis > MAX_HISTORY_SCAN_TIME) {
@ -158,12 +143,21 @@ public class AutoDownloadVerticle extends AbstractVerticle {
addHistoryMessage(auto, currentTimeMillis); addHistoryMessage(auto, currentTimeMillis);
} else { } else {
log.debug("%s No more history files found! TelegramId: %d ChatId: %d".formatted(auto.uniqueKey(), auto.telegramId, auto.chatId)); 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 { } else {
DataVerticle.fileRepository.getFilesByUniqueId(TdApiHelp.getFileUniqueIds(Arrays.asList(foundChatMessages.messages))) DataVerticle.fileRepository.getFilesByUniqueId(TdApiHelp.getFileUniqueIds(Arrays.asList(foundChatMessages.messages)))
.onSuccess(existFiles -> { .onSuccess(existFiles -> {
List<TdApi.Message> messages = Stream.of(foundChatMessages.messages) List<TdApi.Message> 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(); .toList();
if (CollUtil.isEmpty(messages)) { if (CollUtil.isEmpty(messages)) {
auto.nextFromMessageId = foundChatMessages.nextFromMessageId; auto.nextFromMessageId = foundChatMessages.nextFromMessageId;
@ -259,7 +253,7 @@ public class AutoDownloadVerticle extends AbstractVerticle {
long telegramId = jsonObject.getLong("telegramId"); long telegramId = jsonObject.getLong("telegramId");
long chatId = jsonObject.getLong("chatId"); long chatId = jsonObject.getLong("chatId");
long messageId = jsonObject.getLong("messageId"); long messageId = jsonObject.getLong("messageId");
autoRecords.items.stream() autoRecords.getDownloadEnabledItems().stream()
.filter(item -> item.telegramId == telegramId && item.chatId == chatId) .filter(item -> item.telegramId == telegramId && item.chatId == chatId)
.findFirst() .findFirst()
.flatMap(item -> TelegramVerticles.get(telegramId)) .flatMap(item -> TelegramVerticles.get(telegramId))

View file

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.log.Log; import cn.hutool.log.Log;
import cn.hutool.log.LogFactory; import cn.hutool.log.LogFactory;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.json.Json;
import telegram.files.repository.SettingAutoRecords; import telegram.files.repository.SettingAutoRecords;
import telegram.files.repository.SettingKey; import telegram.files.repository.SettingKey;
@ -18,7 +19,11 @@ public class AutoRecordsHolder {
private final List<Consumer<List<SettingAutoRecords.Item>>> onRemoveListeners = new ArrayList<>(); private final List<Consumer<List<SettingAutoRecords.Item>>> onRemoveListeners = new ArrayList<>();
public AutoRecordsHolder() { private volatile boolean initialized = false;
public static final AutoRecordsHolder INSTANCE = new AutoRecordsHolder();
private AutoRecordsHolder() {
} }
public SettingAutoRecords autoRecords() { public SettingAutoRecords autoRecords() {
@ -29,9 +34,13 @@ public class AutoRecordsHolder {
onRemoveListeners.add(onRemove); onRemoveListeners.add(onRemove);
} }
public Future<Void> init() { public synchronized Future<Void> init() {
if (initialized) {
return Future.succeededFuture();
}
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload) return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
.onSuccess(settingAutoRecords -> { .onSuccess(settingAutoRecords -> {
initialized = true;
if (settingAutoRecords == null) { if (settingAutoRecords == null) {
return; return;
} }
@ -77,4 +86,17 @@ public class AutoRecordsHolder {
onRemoveListeners.forEach(listener -> listener.accept(removedItems)); onRemoveListeners.forEach(listener -> listener.accept(removedItems));
} }
} }
public Future<Void> saveAutoRecords() {
return DataVerticle.settingRepository.<SettingAutoRecords>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();
}
} }

View file

@ -44,8 +44,6 @@ public class HttpVerticle extends AbstractVerticle {
// session id -> telegram verticle // session id -> telegram verticle
private final Map<String, TelegramVerticle> sessionTelegramVerticles = new ConcurrentHashMap<>(); private final Map<String, TelegramVerticle> sessionTelegramVerticles = new ConcurrentHashMap<>();
private final AutoRecordsHolder autoRecordsHolder = new AutoRecordsHolder();
private final FileRouteHandler fileRouteHandler = new FileRouteHandler(); private final FileRouteHandler fileRouteHandler = new FileRouteHandler();
private static final String SESSION_COOKIE_NAME = "tf"; private static final String SESSION_COOKIE_NAME = "tf";
@ -54,17 +52,22 @@ public class HttpVerticle extends AbstractVerticle {
public void start(Promise<Void> startPromise) { public void start(Promise<Void> startPromise) {
initHttpServer() initHttpServer()
.compose(r -> initTelegramVerticles()) .compose(r -> initTelegramVerticles())
.compose(r -> autoRecordsHolder.init()) .compose(r -> AutoRecordsHolder.INSTANCE.init())
.compose(r -> initAutoDownloadVerticle()) .compose(r -> initAutoDownloadVerticle())
.compose(r -> initTransferVerticle()) .compose(r -> initTransferVerticle())
.compose(r -> initPreloadMessageVerticle())
.compose(r -> initEventConsumer()) .compose(r -> initEventConsumer())
.onSuccess(startPromise::complete) .onSuccess(startPromise::complete)
.onFailure(startPromise::fail); .onFailure(startPromise::fail);
} }
@Override @Override
public void stop() { public void stop(Promise<Void> stopPromise) {
AutoRecordsHolder.INSTANCE.saveAutoRecords()
.onComplete(ignore -> {
log.info("Http verticle stopped!"); log.info("Http verticle stopped!");
stopPromise.complete();
});
} }
public Future<Void> initHttpServer() { public Future<Void> initHttpServer() {
@ -153,7 +156,7 @@ public class HttpVerticle extends AbstractVerticle {
router.post("/:telegramId/file/cancel-download").handler(this::handleFileCancelDownload); router.post("/:telegramId/file/cancel-download").handler(this::handleFileCancelDownload);
router.post("/:telegramId/file/toggle-pause-download").handler(this::handleFileTogglePauseDownload); router.post("/:telegramId/file/toggle-pause-download").handler(this::handleFileTogglePauseDownload);
router.post("/:telegramId/file/remove").handler(this::handleFileRemove); 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() router.route()
.failureHandler(ctx -> { .failureHandler(ctx -> {
@ -182,15 +185,19 @@ public class HttpVerticle extends AbstractVerticle {
} }
public Future<Void> initAutoDownloadVerticle() { public Future<Void> initAutoDownloadVerticle() {
return vertx.deployVerticle(new AutoDownloadVerticle(autoRecordsHolder), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS) return vertx.deployVerticle(new AutoDownloadVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
.mapEmpty(); .mapEmpty();
} }
public Future<Void> initTransferVerticle() { public Future<Void> initTransferVerticle() {
return vertx.deployVerticle(new TransferVerticle(autoRecordsHolder), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS) return vertx.deployVerticle(new TransferVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
.mapEmpty(); .mapEmpty();
} }
public Future<Void> initPreloadMessageVerticle() {
return vertx.deployVerticle(new PreloadMessageVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
.mapEmpty();
}
private Future<Void> initEventConsumer() { private Future<Void> initEventConsumer() {
vertx.eventBus().consumer(EventEnum.TELEGRAM_EVENT.address(), message -> { 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 -> { vertx.eventBus().consumer(EventEnum.AUTO_DOWNLOAD_UPDATE.address(), message -> {
log.debug("Auto download update: %s".formatted(message.body())); log.debug("Auto settings update: %s".formatted(message.body()));
autoRecordsHolder.onAutoRecordsUpdate(Json.decodeValue(message.body().toString(), SettingAutoRecords.class)); AutoRecordsHolder.INSTANCE.onAutoRecordsUpdate(Json.decodeValue(message.body().toString(), SettingAutoRecords.class));
}); });
return Future.succeededFuture(); return Future.succeededFuture();
} }
@ -607,7 +614,7 @@ public class HttpVerticle extends AbstractVerticle {
.onFailure(ctx::fail); .onFailure(ctx::fail);
} }
private void handleAutoDownload(RoutingContext ctx) { private void handleAutoSettingsUpdate(RoutingContext ctx) {
TelegramVerticle telegramVerticle = TelegramVerticles.getOrElseThrow(ctx.pathParam("telegramId")); TelegramVerticle telegramVerticle = TelegramVerticles.getOrElseThrow(ctx.pathParam("telegramId"));
String chatId = ctx.request().getParam("chatId"); String chatId = ctx.request().getParam("chatId");
@ -616,7 +623,7 @@ public class HttpVerticle extends AbstractVerticle {
return; return;
} }
JsonObject params = ctx.body().asJsonObject(); JsonObject params = ctx.body().asJsonObject();
telegramVerticle.toggleAutoDownload(Convert.toLong(chatId), params) telegramVerticle.updateAutoSettings(Convert.toLong(chatId), params)
.onSuccess(r -> ctx.end()) .onSuccess(r -> ctx.end())
.onFailure(ctx::fail); .onFailure(ctx::fail);
} }

View file

@ -85,4 +85,51 @@ public class MessyUtils {
default -> throw new IllegalArgumentException("Unknown unit: " + unit); 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');
}
}
} }

View file

@ -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<Void> 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<Void> 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<TdApiHelp.FileHandler<? extends TdApi.MessageContent>> 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())));
});
}
}

View file

@ -193,7 +193,9 @@ public class TdApiHelp {
case TdApi.MessageDocument.CONSTRUCTOR -> { case TdApi.MessageDocument.CONSTRUCTOR -> {
return Optional.of((T) new DocumentHandler(message)); return Optional.of((T) new DocumentHandler(message));
} }
default -> throw new NoStackTraceException("Unsupported message type: " + message.content.getConstructor()); default -> {
return Optional.empty();
}
} }
} }

View file

@ -312,11 +312,15 @@ public class TelegramVerticle extends AbstractVerticle {
TdApiHelp.FileHandler<? extends TdApi.MessageContent> fileHandler = TdApiHelp.getFileHandler(message) TdApiHelp.FileHandler<? extends TdApi.MessageContent> fileHandler = TdApiHelp.getFileHandler(message)
.orElseThrow(() -> new NoStackTraceException("not support message type")); .orElseThrow(() -> new NoStackTraceException("not support message type"));
FileRecord fileRecord = fileHandler.convertFileRecord(telegramRecord.id()); FileRecord fileRecord = fileHandler.convertFileRecord(telegramRecord.id());
return DataVerticle.fileRepository.create(fileRecord) return DataVerticle.fileRepository.createIfNotExist(fileRecord)
.compose(r -> .compose(r -> {
client.execute(new TdApi.AddFileToDownloads(fileId, chatId, messageId, 32)) if (!r) {
) return DataVerticle.fileRepository.updateFileId(fileRecord.id(), fileRecord.uniqueId());
.onSuccess(r -> }
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() sendEvent(EventPayload.build(EventPayload.TYPE_FILE_STATUS, new JsonObject()
.put("fileId", fileId) .put("fileId", fileId)
.put("uniqueId", fileRecord.uniqueId()) .put("uniqueId", fileRecord.uniqueId())
@ -430,24 +434,33 @@ public class TelegramVerticle extends AbstractVerticle {
.mapEmpty(); .mapEmpty();
} }
public Future<Void> toggleAutoDownload(Long chatId, JsonObject params) { public Future<Void> updateAutoSettings(Long chatId, JsonObject params) {
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload) return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
.compose(settingAutoRecords -> { .compose(settingAutoRecords -> {
if (settingAutoRecords == null) { if (settingAutoRecords == null) {
settingAutoRecords = new SettingAutoRecords(); settingAutoRecords = new SettingAutoRecords();
} }
if (settingAutoRecords.exists(this.telegramRecord.id(), chatId)) { SettingAutoRecords.Rule rule = params.getJsonObject("rule").mapTo(SettingAutoRecords.Rule.class);
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) { if (StrUtil.isBlank(rule.query) && CollUtil.isEmpty(rule.fileTypes) && rule.transferRule == null) {
rule = 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);
} }
settingAutoRecords.add(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)); return DataVerticle.settingRepository.createOrUpdate(SettingKey.autoDownload.name(), Json.encode(settingAutoRecords));
}) })
.onSuccess(r -> vertx.eventBus().publish(EventEnum.AUTO_DOWNLOAD_UPDATE.name(), r.value())) .onSuccess(r -> vertx.eventBus().publish(EventEnum.AUTO_DOWNLOAD_UPDATE.name(), r.value()))
@ -807,12 +820,10 @@ public class TelegramVerticle extends AbstractVerticle {
//<-----------------------------convert----------------------------------> //<-----------------------------convert---------------------------------->
private Future<JsonArray> convertChat(List<TdApi.Chat> chats) { private Future<JsonArray> convertChat(List<TdApi.Chat> chats) {
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload) Map<Long, SettingAutoRecords.Item> enableAutoChats = AutoRecordsHolder.INSTANCE.autoRecords().getItems(this.telegramRecord.id());
.map(settingAutoRecords -> settingAutoRecords == null ? new HashMap<Long, SettingAutoRecords.Item>() return Future.succeededFuture(new JsonArray(chats.stream()
: settingAutoRecords.getItems(this.telegramRecord.id()))
.map(enableAutoChats -> new JsonArray(chats.stream()
.map(chat -> { .map(chat -> {
SettingAutoRecords.Item autoItem = enableAutoChats.get(chat.id); SettingAutoRecords.Item auto = enableAutoChats.get(chat.id);
return new JsonObject() return new JsonObject()
.put("id", Convert.toStr(chat.id)) .put("id", Convert.toStr(chat.id))
.put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title) .put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title)
@ -821,8 +832,7 @@ public class TelegramVerticle extends AbstractVerticle {
.put("unreadCount", chat.unreadCount) .put("unreadCount", chat.unreadCount)
.put("lastMessage", "") .put("lastMessage", "")
.put("lastMessageTime", "") .put("lastMessageTime", "")
.put("autoEnabled", autoItem != null) .put("auto", auto);
.put("autoRule", autoItem == null ? null : autoItem.rule);
}) })
.toList() .toList()
)); ));

View file

@ -38,9 +38,9 @@ public class TransferVerticle extends AbstractVerticle {
private volatile Transfer beingTransferred; private volatile Transfer beingTransferred;
public TransferVerticle(AutoRecordsHolder autoRecordsHolder) { public TransferVerticle() {
this.autoRecords = autoRecordsHolder.autoRecords(); this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords();
autoRecordsHolder.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> { AutoRecordsHolder.INSTANCE.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> {
waitingTransferFiles.removeIf(waitingTransferFile -> waitingTransferFile.uniqueId().equals(item.uniqueKey())); waitingTransferFiles.removeIf(waitingTransferFile -> waitingTransferFile.uniqueId().equals(item.uniqueKey()));
transfers.remove(item.uniqueKey()); transfers.remove(item.uniqueKey());
})); }));
@ -109,7 +109,7 @@ public class TransferVerticle extends AbstractVerticle {
log.debug("Start scan history files for transfer"); log.debug("Start scan history files for transfer");
for (SettingAutoRecords.Item item : autoRecords.items) { for (SettingAutoRecords.Item item : autoRecords.items) {
Transfer transfer = getTransfer(item); Transfer transfer = getTransfer(item);
if (transfer == null || !transfer.transferHistory) { if (transfer == null || !transfer.transferHistory || item.isNotComplete(SettingAutoRecords.HISTORY_PRELOAD_STATE)) {
continue; continue;
} }
Tuple3<List<FileRecord>, Long, Long> filesTuple = Future.await(DataVerticle.fileRepository.getFiles(item.chatId, Tuple3<List<FileRecord>, Long, Long> filesTuple = Future.await(DataVerticle.fileRepository.getFiles(item.chatId,
@ -119,6 +119,8 @@ public class TransferVerticle extends AbstractVerticle {
)); ));
List<FileRecord> files = filesTuple.v1; List<FileRecord> files = filesTuple.v1;
if (CollUtil.isEmpty(files)) { if (CollUtil.isEmpty(files)) {
log.debug("No history files found for transfer: %s".formatted(item.uniqueKey()));
item.complete(SettingAutoRecords.HISTORY_TRANSFER_STATE);
continue; continue;
} }

View file

@ -11,6 +11,8 @@ import java.util.Map;
public interface FileRepository { public interface FileRepository {
Future<FileRecord> create(FileRecord fileRecord); Future<FileRecord> create(FileRecord fileRecord);
Future<Boolean> createIfNotExist(FileRecord fileRecord);
Future<Map<Integer, FileRecord>> getFiles(long chatId, List<Integer> fileIds); Future<Map<Integer, FileRecord>> getFiles(long chatId, List<Integer> fileIds);
Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter); Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter);

View file

@ -1,5 +1,7 @@
package telegram.files.repository; package telegram.files.repository;
import com.fasterxml.jackson.annotation.JsonIgnore;
import telegram.files.MessyUtils;
import telegram.files.Transfer; import telegram.files.Transfer;
import java.util.ArrayList; import java.util.ArrayList;
@ -12,6 +14,12 @@ public class SettingAutoRecords {
public List<Item> items; public List<Item> 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 static class Item {
public long telegramId; public long telegramId;
@ -23,7 +31,17 @@ public class SettingAutoRecords {
public Rule rule; public Rule rule;
public boolean downloadEnabled;
public boolean preloadEnabled;
public long nextFromMessageIdForPreload;
public int state;
public Item() { public Item() {
// downloadEnabled default is true
this.downloadEnabled = true;
} }
public Item(long telegramId, long chatId, Rule rule) { public Item(long telegramId, long chatId, Rule rule) {
@ -35,6 +53,17 @@ public class SettingAutoRecords {
public String uniqueKey() { public String uniqueKey() {
return telegramId + ":" + chatId; 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 { public static class Rule {
@ -102,6 +131,20 @@ public class SettingAutoRecords {
items.removeIf(item -> item.telegramId == telegramId && item.chatId == chatId); items.removeIf(item -> item.telegramId == telegramId && item.chatId == chatId);
} }
@JsonIgnore
public List<Item> getDownloadEnabledItems() {
return items.stream()
.filter(i -> i.downloadEnabled)
.toList();
}
@JsonIgnore
public List<Item> getPreloadEnabledItems() {
return items.stream()
.filter(i -> i.preloadEnabled)
.toList();
}
public Map<Long, Item> getItems(long telegramId) { public Map<Long, Item> getItems(long telegramId) {
return items.stream() return items.stream()
.filter(item -> item.telegramId == telegramId) .filter(item -> item.telegramId == telegramId)

View file

@ -57,11 +57,19 @@ public class FileRepositoryImpl implements FileRepository {
.execute(fileRecord) .execute(fileRecord)
.map(r -> fileRecord) .map(r -> fileRecord)
.compose(r -> this.updateCaptionByMediaAlbumId(fileRecord.mediaAlbumId(), fileRecord.caption()).map(r)) .compose(r -> this.updateCaptionByMediaAlbumId(fileRecord.mediaAlbumId(), fileRecord.caption()).map(r))
.onSuccess(r -> log.trace("Successfully created file record: %s".formatted(fileRecord.id())) .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())));
.onFailure( }
err -> log.error("Failed to create file record: %s".formatted(err.getMessage()))
); @Override
public Future<Boolean> createIfNotExist(FileRecord fileRecord) {
return this.getByUniqueId(fileRecord.uniqueId())
.compose(record -> {
if (record != null) {
return Future.succeededFuture(false);
}
return this.create(fileRecord).map(true);
});
} }
@Override @Override

View file

@ -28,7 +28,7 @@ public class AutoRecordsHolderTest {
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
autoRecordsHolder = new AutoRecordsHolder(); autoRecordsHolder = AutoRecordsHolder.INSTANCE;
telegramVerticlesMockedStatic = mockStatic(TelegramVerticles.class); telegramVerticlesMockedStatic = mockStatic(TelegramVerticles.class);
// Prepare test data // Prepare test data

View file

@ -8,16 +8,19 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { type TelegramChat } from "@/lib/types";
interface AutoDownloadButtonProps interface AutoDownloadButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> { extends React.ButtonHTMLAttributes<HTMLButtonElement> {
autoEnabled?: boolean; auto?: TelegramChat["auto"];
} }
const AutoDownloadButton = React.forwardRef< const AutoDownloadButton = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
AutoDownloadButtonProps AutoDownloadButtonProps
>(({ autoEnabled = false, className, ...props }, ref) => { >(({ auto, className, ...props }, ref) => {
const autoEnabled = auto && (auto.downloadEnabled || auto.preloadEnabled);
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
@ -40,15 +43,19 @@ const AutoDownloadButton = React.forwardRef<
<div <div
className={cn( className={cn(
"absolute left-[10%] top-[40%] h-2 w-2 scale-[1] rounded-lg bg-primary transition-all duration-300 group-hover:left-[0%] group-hover:top-[0%] group-hover:h-full group-hover:w-full group-hover:scale-[1.8] group-hover:animate-none group-hover:bg-primary", "absolute left-[10%] top-[40%] h-2 w-2 scale-[1] rounded-lg bg-primary transition-all duration-300 group-hover:left-[0%] group-hover:top-[0%] group-hover:h-full group-hover:w-full group-hover:scale-[1.8] group-hover:animate-none group-hover:bg-primary",
autoEnabled ? "animate-breathing bg-green-500" : "bg-red-500", autoEnabled
? auto?.downloadEnabled && auto.preloadEnabled
? "animate-breathing bg-green-500"
: "animate-breathing bg-blue-500"
: "bg-red-500",
)} )}
></div> ></div>
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{autoEnabled {autoEnabled
? "Auto download is enabled, you can disable it by clicking the button" ? "Automation is enabled, you can disable it by clicking the button"
: "Auto download is disabled, you can enable it by clicking the button"} : "Automation is disabled, you can enable it by clicking the button"}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View file

@ -8,7 +8,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; 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 useSWRMutation from "swr/mutation";
import { POST } from "@/lib/api"; import { POST } from "@/lib/api";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
@ -55,6 +55,9 @@ export default function AutoDownloadDialog() {
const { isLoading, chat, reload } = useTelegramChat(); const { isLoading, chat, reload } = useTelegramChat();
const { toast } = useToast(); const { toast } = useToast();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editMode, setEditMode] = useState(false);
const [downloadEnabled, setDownloadEnabled] = useState(false);
const [preloadEnabled, setPreloadEnabled] = useState(false);
const [rule, setRule] = useState<AutoDownloadRule>({ const [rule, setRule] = useState<AutoDownloadRule>({
query: "", query: "",
fileTypes: [], fileTypes: [],
@ -62,18 +65,28 @@ export default function AutoDownloadDialog() {
const { trigger: triggerAuto, isMutating: isAutoMutating } = useSWRMutation( const { trigger: triggerAuto, isMutating: isAutoMutating } = useSWRMutation(
!accountId || !chat !accountId || !chat
? undefined ? undefined
: `/${accountId}/file/auto-download?telegramId=${accountId}&chatId=${chat?.id}`, : `/${accountId}/file/update-auto-settings?telegramId=${accountId}&chatId=${chat?.id}`,
(key, { arg }: { arg: { rule: AutoDownloadRule } }) => { (
key,
{
arg,
}: {
arg: {
downloadEnabled: boolean;
preloadEnabled: boolean;
rule: AutoDownloadRule;
};
},
) => {
return POST(key, arg); return POST(key, arg);
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast({ toast({
title: chat?.autoEnabled title: "Auto settings updated!",
? "Auto download disabled for this chat!"
: "Auto download enabled for this chat!",
}); });
void reload(); void reload();
setEditMode(false);
setTimeout(() => { setTimeout(() => {
setOpen(false); setOpen(false);
}, 1000); }, 1000);
@ -85,6 +98,18 @@ export default function AutoDownloadDialog() {
leading: true, 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) { if (isLoading) {
return ( return (
<div className="h-8 w-32 animate-pulse bg-gray-200 dark:bg-gray-700"></div> <div className="h-8 w-32 animate-pulse bg-gray-200 dark:bg-gray-700"></div>
@ -100,38 +125,69 @@ export default function AutoDownloadDialog() {
setOpen(!open); setOpen(!open);
}} }}
> >
{chat && <AutoDownloadButton autoEnabled={chat.autoEnabled} />} {chat && <AutoDownloadButton auto={chat.auto} />}
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
aria-describedby={undefined} aria-describedby={undefined}
onPointerDownOutside={() => setOpen(false)} onPointerDownOutside={() => setOpen(false)}
onClick={(e) => e.stopPropagation()} 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]"
> >
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{chat?.autoEnabled Update Auto Settings for {chat?.name ?? "Unknown Chat"}
? "Disable Auto Download"
: "Enable auto download for this chat?"}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<DialogDescription></DialogDescription> <DialogDescription></DialogDescription>
{chat?.autoEnabled ? ( {!editMode && chat?.auto ? (
<div className="space-y-4"> <div className="space-y-4">
{chat.autoRule && ( <div className="space-y-4 rounded-md border border-gray-200 p-4 dark:border-gray-700">
<div className="space-y-4"> <div className="flex items-center justify-between">
<div className="flex items-center">
<Label className="text-sm font-semibold text-gray-900 dark:text-gray-300"> <Label className="text-sm font-semibold text-gray-900 dark:text-gray-300">
Current Rule Auto preload
</Label> </Label>
<Badge <Badge
variant="outline" variant="outline"
className="ml-2 border-none bg-green-500 px-2 py-0.5 text-xs text-white dark:bg-green-800 dark:text-green-200" className={cn(
"border-none bg-green-500 px-2 py-0.5 text-xs text-white dark:bg-green-800 dark:text-green-200",
chat.auto.preloadEnabled
? "bg-green-500 dark:bg-green-800 dark:text-green-200"
: "bg-gray-500 dark:bg-gray-800 dark:text-gray-300",
)}
> >
Active {chat.auto.preloadEnabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
</div> </div>
{(chat.auto.state & (1 << 1)) != 0 && (
<p className="text-xs text-muted-foreground">
All historical files are preloaded.
</p>
)}
</div>
<div className="space-y-4 rounded-md border border-gray-200 p-4 dark:border-gray-700">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold text-gray-900 dark:text-gray-300">
Auto download
</Label>
<Badge
variant="outline"
className={cn(
"border-none px-2 py-0.5 text-xs text-white",
chat.auto.downloadEnabled
? "bg-green-500 dark:bg-green-800 dark:text-green-200"
: "bg-gray-500 dark:bg-gray-800 dark:text-gray-300",
)}
>
{chat.auto.downloadEnabled ? "Enabled" : "Disabled"}
</Badge>
</div>
{(chat.auto.state & (1 << 2)) != 0 && (
<p className="text-xs text-muted-foreground">
All historical files are downloaded.
</p>
)}
{chat.auto.rule && (
<div className="space-y-4">
<div className="space-y-3"> <div className="space-y-3">
{/* Filter Keyword Section */} {/* Filter Keyword Section */}
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"> <div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
@ -140,7 +196,7 @@ export default function AutoDownloadDialog() {
Filter Keyword Filter Keyword
</span> </span>
<span className="text-sm text-gray-500 dark:text-gray-300"> <span className="text-sm text-gray-500 dark:text-gray-300">
{chat.autoRule.query || "No keyword specified"} {chat.auto.rule.query || "No keyword specified"}
</span> </span>
</div> </div>
</div> </div>
@ -150,8 +206,8 @@ export default function AutoDownloadDialog() {
File Types File Types
</span> </span>
<div className="mt-2 flex flex-wrap gap-2"> <div className="mt-2 flex flex-wrap gap-2">
{chat.autoRule.fileTypes.length > 0 ? ( {chat.auto.rule.fileTypes.length > 0 ? (
chat.autoRule.fileTypes.map((type) => ( chat.auto.rule.fileTypes.map((type) => (
<Badge <Badge
key={type} key={type}
variant="secondary" variant="secondary"
@ -169,7 +225,7 @@ export default function AutoDownloadDialog() {
</div> </div>
{/* Transfer Rule Section */} {/* Transfer Rule Section */}
{chat.autoRule.transferRule && ( {chat.auto.rule.transferRule && (
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"> <div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<span className="text-xs font-medium text-gray-500"> <span className="text-xs font-medium text-gray-500">
@ -181,7 +237,7 @@ export default function AutoDownloadDialog() {
Destination Folder Destination Folder
</span> </span>
<span className="rounded border border-gray-200 px-1.5 py-0.5 text-sm dark:border-gray-700 dark:bg-gray-800"> <span className="rounded border border-gray-200 px-1.5 py-0.5 text-sm dark:border-gray-700 dark:bg-gray-800">
{chat.autoRule.transferRule.destination} {chat.auto.rule.transferRule.destination}
</span> </span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -189,7 +245,7 @@ export default function AutoDownloadDialog() {
Transfer Policy Transfer Policy
</span> </span>
<Badge variant="outline" className="font-normal"> <Badge variant="outline" className="font-normal">
{chat.autoRule.transferRule.transferPolicy} {chat.auto.rule.transferRule.transferPolicy}
</Badge> </Badge>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -197,7 +253,7 @@ export default function AutoDownloadDialog() {
Duplication Policy Duplication Policy
</span> </span>
<Badge variant="outline" className="font-normal"> <Badge variant="outline" className="font-normal">
{chat.autoRule.transferRule.duplicationPolicy} {chat.auto.rule.transferRule.duplicationPolicy}
</Badge> </Badge>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -205,7 +261,7 @@ export default function AutoDownloadDialog() {
Transfer History Transfer History
</span> </span>
<Badge> <Badge>
{chat.autoRule.transferRule.transferHistory {chat.auto.rule.transferRule.transferHistory
? "Enabled" ? "Enabled"
: "Disabled"} : "Disabled"}
</Badge> </Badge>
@ -217,38 +273,66 @@ export default function AutoDownloadDialog() {
</div> </div>
</div> </div>
)} )}
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span>
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
This will disable auto download for this chat. You can always
enable it later.
</p>
</div>
<div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span>
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
Files that are being downloaded will be paused and you can
enable automatic downloading again later.
</p>
</div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-4 rounded-md border border-gray-200 p-4 dark:border-gray-700">
<div className="flex items-center justify-between">
<Label htmlFor="enable-preload">Enable Preload</Label>
<Switch
id="enable-preload"
checked={preloadEnabled}
onCheckedChange={setPreloadEnabled}
/>
</div>
{preloadEnabled && (
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800"> <div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300"> <p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
This will enable auto download for this chat. Files will be This will enable preload for this chat. All files will be
downloaded automatically. loaded, but not downloaded, then you can search offline.
</p>
</div>
</div>
)}
</div>
<div className="space-y-4 rounded-md border border-gray-200 p-4 dark:border-gray-700">
<div className="flex items-center justify-between">
<Label htmlFor="enable-auto-download">
Enable Auto Download
</Label>
<Switch
id="enable-auto-download"
checked={downloadEnabled}
onCheckedChange={(checked) => {
if (checked) {
setRule({
query: "",
fileTypes: [],
});
}
setDownloadEnabled(checked);
}}
/>
</div>
{downloadEnabled && (
<>
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
<div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
This will enable auto download for this chat. Files will
be downloaded automatically.
</p> </p>
</div> </div>
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300"> <p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
Files in historical messages will be downloaded first, and Files in historical messages will be downloaded first,
then files in new messages will be downloaded automatically. and then files in new messages will be downloaded
automatically.
</p> </p>
</div> </div>
<div className="flex items-start"> <div className="flex items-start">
@ -262,9 +346,18 @@ export default function AutoDownloadDialog() {
</div> </div>
</div> </div>
<AutoDownloadRule value={rule} onChange={setRule} /> <AutoDownloadRule value={rule} onChange={setRule} />
</>
)}
</div>
</div> </div>
)} )}
<DialogFooter className="gap-2"> <DialogFooter className="gap-2">
{!editMode && chat?.auto ? (
<Button variant="outline" onClick={() => setEditMode(true)}>
Edit
</Button>
) : (
<>
<Button <Button
variant="outline" variant="outline"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
@ -274,28 +367,27 @@ export default function AutoDownloadDialog() {
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
const folderPathRegex = /^[\/\\]?(?:[^<>:"|?*\/\\]+[\/\\]?)*$/; const folderPathRegex =
/^[\/\\]?(?:[^<>:"|?*\/\\]+[\/\\]?)*$/;
if ( if (
rule.transferRule && rule.transferRule &&
!folderPathRegex.test(rule.transferRule.destination) !folderPathRegex.test(rule.transferRule.destination)
) { ) {
toast({ toast({
title: "Invalid destination folder", title: "Invalid destination folder",
description: "Please enter a valid destination folder path", description:
"Please enter a valid destination folder path",
}); });
return; return;
} }
void triggerAuto({ rule }); void triggerAuto({ downloadEnabled, preloadEnabled, rule });
}} }}
variant={chat?.autoEnabled ? "destructive" : "default"}
disabled={debounceIsAutoMutating} disabled={debounceIsAutoMutating}
> >
{debounceIsAutoMutating {debounceIsAutoMutating ? "Submitting..." : "Submit"}
? "Submitting..."
: chat?.autoEnabled
? "Disable"
: "Enable"}
</Button> </Button>
</>
)}
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@ -546,11 +638,13 @@ const PolicyLegends: Record<
}, },
OVERWRITE: { OVERWRITE: {
title: "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: { SKIP: {
title: "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: { RENAME: {
title: "Rename", title: "Rename",

View file

@ -10,6 +10,7 @@ import useSWR from "swr";
import { Ellipsis } from "lucide-react"; import { Ellipsis } from "lucide-react";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import * as React from "react"; import * as React from "react";
import { useEffect } from "react";
interface FileTypeFilterProps { interface FileTypeFilterProps {
offline: boolean; offline: boolean;
@ -55,6 +56,13 @@ export default function FileTypeFilter({
); );
}; };
useEffect(() => {
if (!offline && localType === "all") {
setLocalType("media");
onChange("media");
}
}, [localType, offline, onChange]);
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<Label>Type</Label> <Label>Type</Label>
@ -63,7 +71,7 @@ export default function FileTypeFilter({
<SelectValue placeholder="File type" /> <SelectValue placeholder="File type" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{offline && <SelectItem value="all">All files</SelectItem>} {offline && <SelectItem value="all">All Files</SelectItem>}
<FileTypeSelectItem value="media" /> <FileTypeSelectItem value="media" />
<FileTypeSelectItem value="photo" /> <FileTypeSelectItem value="photo" />
<FileTypeSelectItem value="video" /> <FileTypeSelectItem value="video" />

View file

@ -6,7 +6,7 @@ export default function TelegramIcon({ className }: { className?: string }) {
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
className={cn(className, "dark:fill-current")} className={cn(className, "dark:fill-white")}
> >
<title>Telegram</title> <title>Telegram</title>
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" /> <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />

View file

@ -19,8 +19,12 @@ export type TelegramChat = {
unreadCount?: number; unreadCount?: number;
lastMessage?: string; lastMessage?: string;
lastMessageTime?: string; lastMessageTime?: string;
autoEnabled: boolean; auto?: {
autoRule?: AutoDownloadRule; preloadEnabled: boolean;
downloadEnabled: boolean;
rule?: AutoDownloadRule;
state: number;
};
}; };
export type FileType = "media" | "photo" | "video" | "audio" | "file"; export type FileType = "media" | "photo" | "video" | "audio" | "file";