✨ feat: Support preload file messages.
This commit is contained in:
parent
23fd65b7f0
commit
6431ce7846
17 changed files with 642 additions and 272 deletions
|
|
@ -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()
|
log.info("Auto download verticle stopped!");
|
||||||
.onComplete(r -> {
|
|
||||||
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))
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
log.info("Http verticle stopped!");
|
AutoRecordsHolder.INSTANCE.saveAutoRecords()
|
||||||
|
.onComplete(ignore -> {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
120
api/src/main/java/telegram/files/PreloadMessageVerticle.java
Normal file
120
api/src/main/java/telegram/files/PreloadMessageVerticle.java
Normal 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())));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
if (StrUtil.isBlank(rule.query) && CollUtil.isEmpty(rule.fileTypes) && rule.transferRule == null) {
|
||||||
} else {
|
rule = null;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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));
|
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,25 +820,22 @@ 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(chat -> {
|
||||||
.map(enableAutoChats -> new JsonArray(chats.stream()
|
SettingAutoRecords.Item auto = enableAutoChats.get(chat.id);
|
||||||
.map(chat -> {
|
return new JsonObject()
|
||||||
SettingAutoRecords.Item autoItem = enableAutoChats.get(chat.id);
|
.put("id", Convert.toStr(chat.id))
|
||||||
return new JsonObject()
|
.put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title)
|
||||||
.put("id", Convert.toStr(chat.id))
|
.put("type", TdApiHelp.getChatType(chat.type))
|
||||||
.put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title)
|
.put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(chat, "photo.minithumbnail.data")))
|
||||||
.put("type", TdApiHelp.getChatType(chat.type))
|
.put("unreadCount", chat.unreadCount)
|
||||||
.put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(chat, "photo.minithumbnail.data")))
|
.put("lastMessage", "")
|
||||||
.put("unreadCount", chat.unreadCount)
|
.put("lastMessageTime", "")
|
||||||
.put("lastMessage", "")
|
.put("auto", auto);
|
||||||
.put("lastMessageTime", "")
|
})
|
||||||
.put("autoEnabled", autoItem != null)
|
.toList()
|
||||||
.put("autoRule", autoItem == null ? null : autoItem.rule);
|
));
|
||||||
})
|
|
||||||
.toList()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Future<JsonObject> convertFiles(Tuple2<TdApi.FoundChatMessages, Map<String, FileRecord>> tuple) {
|
private Future<JsonObject> convertFiles(Tuple2<TdApi.FoundChatMessages, Map<String, FileRecord>> tuple) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,202 +125,269 @@ 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">
|
Auto preload
|
||||||
Current Rule
|
</Label>
|
||||||
</Label>
|
<Badge
|
||||||
<Badge
|
variant="outline"
|
||||||
variant="outline"
|
className={cn(
|
||||||
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"
|
"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
|
||||||
Active
|
? "bg-green-500 dark:bg-green-800 dark:text-green-200"
|
||||||
</Badge>
|
: "bg-gray-500 dark:bg-gray-800 dark:text-gray-300",
|
||||||
</div>
|
)}
|
||||||
|
>
|
||||||
<div className="space-y-3">
|
{chat.auto.preloadEnabled ? "Enabled" : "Disabled"}
|
||||||
{/* Filter Keyword Section */}
|
</Badge>
|
||||||
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
|
</div>
|
||||||
<div className="flex flex-col space-y-1">
|
{(chat.auto.state & (1 << 1)) != 0 && (
|
||||||
<span className="text-xs font-medium text-gray-500">
|
<p className="text-xs text-muted-foreground">
|
||||||
Filter Keyword
|
All historical files are preloaded.
|
||||||
</span>
|
</p>
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-300">
|
)}
|
||||||
{chat.autoRule.query || "No keyword specified"}
|
</div>
|
||||||
</span>
|
<div className="space-y-4 rounded-md border border-gray-200 p-4 dark:border-gray-700">
|
||||||
</div>
|
<div className="flex items-center justify-between">
|
||||||
</div>
|
<Label className="text-sm font-semibold text-gray-900 dark:text-gray-300">
|
||||||
|
Auto download
|
||||||
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
|
</Label>
|
||||||
<span className="text-xs font-medium text-gray-500">
|
<Badge
|
||||||
File Types
|
variant="outline"
|
||||||
</span>
|
className={cn(
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
"border-none px-2 py-0.5 text-xs text-white",
|
||||||
{chat.autoRule.fileTypes.length > 0 ? (
|
chat.auto.downloadEnabled
|
||||||
chat.autoRule.fileTypes.map((type) => (
|
? "bg-green-500 dark:bg-green-800 dark:text-green-200"
|
||||||
<Badge
|
: "bg-gray-500 dark:bg-gray-800 dark:text-gray-300",
|
||||||
key={type}
|
)}
|
||||||
variant="secondary"
|
>
|
||||||
className="flex items-center gap-1 border-gray-200 bg-white px-3 py-1 capitalize text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
{chat.auto.downloadEnabled ? "Enabled" : "Disabled"}
|
||||||
>
|
</Badge>
|
||||||
{type}
|
</div>
|
||||||
</Badge>
|
{(chat.auto.state & (1 << 2)) != 0 && (
|
||||||
))
|
<p className="text-xs text-muted-foreground">
|
||||||
) : (
|
All historical files are downloaded.
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-300">
|
</p>
|
||||||
No file types selected
|
)}
|
||||||
</span>
|
{chat.auto.rule && (
|
||||||
)}
|
<div className="space-y-4">
|
||||||
</div>
|
<div className="space-y-3">
|
||||||
</div>
|
{/* Filter Keyword Section */}
|
||||||
|
|
||||||
{/* Transfer Rule Section */}
|
|
||||||
{chat.autoRule.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-1">
|
||||||
<span className="text-xs font-medium text-gray-500">
|
<span className="text-xs font-medium text-gray-500">
|
||||||
Transfer Rule
|
Filter Keyword
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
<span className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
<div className="flex flex-col space-y-1">
|
{chat.auto.rule.query || "No keyword specified"}
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-300">
|
</span>
|
||||||
Destination Folder
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<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}
|
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||||
</span>
|
<span className="text-xs font-medium text-gray-500">
|
||||||
</div>
|
File Types
|
||||||
<div className="flex items-center justify-between">
|
</span>
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-300">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
Transfer Policy
|
{chat.auto.rule.fileTypes.length > 0 ? (
|
||||||
</span>
|
chat.auto.rule.fileTypes.map((type) => (
|
||||||
<Badge variant="outline" className="font-normal">
|
<Badge
|
||||||
{chat.autoRule.transferRule.transferPolicy}
|
key={type}
|
||||||
</Badge>
|
variant="secondary"
|
||||||
</div>
|
className="flex items-center gap-1 border-gray-200 bg-white px-3 py-1 capitalize text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
|
||||||
<div className="flex items-center justify-between">
|
>
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-300">
|
{type}
|
||||||
Duplication Policy
|
|
||||||
</span>
|
|
||||||
<Badge variant="outline" className="font-normal">
|
|
||||||
{chat.autoRule.transferRule.duplicationPolicy}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-300">
|
|
||||||
Transfer History
|
|
||||||
</span>
|
|
||||||
<Badge>
|
|
||||||
{chat.autoRule.transferRule.transferHistory
|
|
||||||
? "Enabled"
|
|
||||||
: "Disabled"}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
|
No file types selected
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Transfer Rule Section */}
|
||||||
|
{chat.auto.rule.transferRule && (
|
||||||
|
<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">
|
||||||
|
<span className="text-xs font-medium text-gray-500">
|
||||||
|
Transfer Rule
|
||||||
|
</span>
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
Destination Folder
|
||||||
|
</span>
|
||||||
|
<span className="rounded border border-gray-200 px-1.5 py-0.5 text-sm dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
{chat.auto.rule.transferRule.destination}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
Transfer Policy
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="font-normal">
|
||||||
|
{chat.auto.rule.transferRule.transferPolicy}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
Duplication Policy
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="font-normal">
|
||||||
|
{chat.auto.rule.transferRule.duplicationPolicy}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-300">
|
||||||
|
Transfer History
|
||||||
|
</span>
|
||||||
|
<Badge>
|
||||||
|
{chat.auto.rule.transferRule.transferHistory
|
||||||
|
? "Enabled"
|
||||||
|
: "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</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 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800">
|
<div className="space-y-4 rounded-md border border-gray-200 p-4 dark:border-gray-700">
|
||||||
<div className="flex items-start">
|
<div className="flex items-center justify-between">
|
||||||
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
|
<Label htmlFor="enable-preload">Enable Preload</Label>
|
||||||
<p className="text-sm leading-6 text-gray-700 dark:text-gray-300">
|
<Switch
|
||||||
This will enable auto download for this chat. Files will be
|
id="enable-preload"
|
||||||
downloaded automatically.
|
checked={preloadEnabled}
|
||||||
</p>
|
onCheckedChange={setPreloadEnabled}
|
||||||
</div>
|
/>
|
||||||
<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">
|
|
||||||
Files in historical messages will be downloaded first, and
|
|
||||||
then files in new messages will be downloaded automatically.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
Download Order:
|
|
||||||
<span className="ml-1 rounded bg-blue-100 px-2 text-blue-700 dark:bg-blue-800 dark:text-blue-200">
|
|
||||||
{"Photo -> Video -> Audio -> File"}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</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="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 preload for this chat. All files will be
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
Files in historical messages will be downloaded first,
|
||||||
|
and then files in new messages will be downloaded
|
||||||
|
automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
Download Order:
|
||||||
|
<span className="ml-1 rounded bg-blue-100 px-2 text-blue-700 dark:bg-blue-800 dark:text-blue-200">
|
||||||
|
{"Photo -> Video -> Audio -> File"}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AutoDownloadRule value={rule} onChange={setRule} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AutoDownloadRule value={rule} onChange={setRule} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<DialogFooter className="gap-2">
|
<DialogFooter className="gap-2">
|
||||||
<Button
|
{!editMode && chat?.auto ? (
|
||||||
variant="outline"
|
<Button variant="outline" onClick={() => setEditMode(true)}>
|
||||||
onClick={() => setOpen(false)}
|
Edit
|
||||||
disabled={debounceIsAutoMutating}
|
</Button>
|
||||||
>
|
) : (
|
||||||
Cancel
|
<>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => setOpen(false)}
|
||||||
const folderPathRegex = /^[\/\\]?(?:[^<>:"|?*\/\\]+[\/\\]?)*$/;
|
disabled={debounceIsAutoMutating}
|
||||||
if (
|
>
|
||||||
rule.transferRule &&
|
Cancel
|
||||||
!folderPathRegex.test(rule.transferRule.destination)
|
</Button>
|
||||||
) {
|
<Button
|
||||||
toast({
|
onClick={() => {
|
||||||
title: "Invalid destination folder",
|
const folderPathRegex =
|
||||||
description: "Please enter a valid destination folder path",
|
/^[\/\\]?(?:[^<>:"|?*\/\\]+[\/\\]?)*$/;
|
||||||
});
|
if (
|
||||||
return;
|
rule.transferRule &&
|
||||||
}
|
!folderPathRegex.test(rule.transferRule.destination)
|
||||||
void triggerAuto({ rule });
|
) {
|
||||||
}}
|
toast({
|
||||||
variant={chat?.autoEnabled ? "destructive" : "default"}
|
title: "Invalid destination folder",
|
||||||
disabled={debounceIsAutoMutating}
|
description:
|
||||||
>
|
"Please enter a valid destination folder path",
|
||||||
{debounceIsAutoMutating
|
});
|
||||||
? "Submitting..."
|
return;
|
||||||
: chat?.autoEnabled
|
}
|
||||||
? "Disable"
|
void triggerAuto({ downloadEnabled, preloadEnabled, rule });
|
||||||
: "Enable"}
|
}}
|
||||||
</Button>
|
disabled={debounceIsAutoMutating}
|
||||||
|
>
|
||||||
|
{debounceIsAutoMutating ? "Submitting..." : "Submit"}
|
||||||
|
</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",
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue