✨ 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.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.Json;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.drinkless.tdlib.TdApi;
|
||||
import org.jooq.lambda.tuple.Tuple2;
|
||||
|
|
@ -47,9 +46,9 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|
|||
|
||||
private int limit = DEFAULT_LIMIT;
|
||||
|
||||
public AutoDownloadVerticle(AutoRecordsHolder autoRecordsHolder) {
|
||||
this.autoRecords = autoRecordsHolder.autoRecords();
|
||||
autoRecordsHolder.registerOnRemoveListener(removedItems -> removedItems.forEach(item ->
|
||||
public AutoDownloadVerticle() {
|
||||
this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords();
|
||||
AutoRecordsHolder.INSTANCE.registerOnRemoveListener(removedItems -> removedItems.forEach(item ->
|
||||
waitingDownloadMessages.getOrDefault(item.telegramId, new LinkedList<>())
|
||||
.removeIf(m -> m.chatId == item.chatId)));
|
||||
}
|
||||
|
|
@ -60,7 +59,10 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|
|||
.compose(v -> this.initEventConsumer())
|
||||
.onSuccess(v -> {
|
||||
vertx.setPeriodic(0, HISTORY_SCAN_INTERVAL,
|
||||
id -> autoRecords.items.forEach(auto -> addHistoryMessage(auto, System.currentTimeMillis())));
|
||||
id -> autoRecords.getDownloadEnabledItems()
|
||||
.stream()
|
||||
.filter(auto -> auto.isNotComplete(SettingAutoRecords.HISTORY_DOWNLOAD_STATE))
|
||||
.forEach(auto -> addHistoryMessage(auto, System.currentTimeMillis())));
|
||||
vertx.setPeriodic(0, DOWNLOAD_INTERVAL,
|
||||
id -> waitingDownloadMessages.keySet().forEach(this::download));
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|
|||
|Download interval: %s ms
|
||||
|Download limit: %s per telegram account!
|
||||
|Auto chats: %s
|
||||
""".formatted(HISTORY_SCAN_INTERVAL, DOWNLOAD_INTERVAL, limit, autoRecords.items.size()));
|
||||
""".formatted(HISTORY_SCAN_INTERVAL, DOWNLOAD_INTERVAL, limit, autoRecords.getDownloadEnabledItems().size()));
|
||||
|
||||
startPromise.complete();
|
||||
})
|
||||
|
|
@ -78,12 +80,8 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void stop(Promise<Void> stopPromise) {
|
||||
saveAutoRecords()
|
||||
.onComplete(r -> {
|
||||
log.info("Auto download verticle stopped!");
|
||||
stopPromise.complete();
|
||||
});
|
||||
public void stop() {
|
||||
log.info("Auto download verticle stopped!");
|
||||
}
|
||||
|
||||
private Future<Void> initAutoDownload() {
|
||||
|
|
@ -109,19 +107,6 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|
|||
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) {
|
||||
log.debug("Start scan history! TelegramId: %d ChatId: %d FileType: %s".formatted(auto.telegramId, auto.chatId, auto.nextFileType));
|
||||
if (System.currentTimeMillis() - currentTimeMillis > MAX_HISTORY_SCAN_TIME) {
|
||||
|
|
@ -158,12 +143,21 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|
|||
addHistoryMessage(auto, currentTimeMillis);
|
||||
} else {
|
||||
log.debug("%s No more history files found! TelegramId: %d ChatId: %d".formatted(auto.uniqueKey(), auto.telegramId, auto.chatId));
|
||||
auto.complete(SettingAutoRecords.HISTORY_DOWNLOAD_STATE);
|
||||
}
|
||||
} else {
|
||||
DataVerticle.fileRepository.getFilesByUniqueId(TdApiHelp.getFileUniqueIds(Arrays.asList(foundChatMessages.messages)))
|
||||
.onSuccess(existFiles -> {
|
||||
List<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();
|
||||
if (CollUtil.isEmpty(messages)) {
|
||||
auto.nextFromMessageId = foundChatMessages.nextFromMessageId;
|
||||
|
|
@ -259,7 +253,7 @@ public class AutoDownloadVerticle extends AbstractVerticle {
|
|||
long telegramId = jsonObject.getLong("telegramId");
|
||||
long chatId = jsonObject.getLong("chatId");
|
||||
long messageId = jsonObject.getLong("messageId");
|
||||
autoRecords.items.stream()
|
||||
autoRecords.getDownloadEnabledItems().stream()
|
||||
.filter(item -> item.telegramId == telegramId && item.chatId == chatId)
|
||||
.findFirst()
|
||||
.flatMap(item -> TelegramVerticles.get(telegramId))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
|||
import cn.hutool.log.Log;
|
||||
import cn.hutool.log.LogFactory;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.Json;
|
||||
import telegram.files.repository.SettingAutoRecords;
|
||||
import telegram.files.repository.SettingKey;
|
||||
|
||||
|
|
@ -18,7 +19,11 @@ public class AutoRecordsHolder {
|
|||
|
||||
private final List<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() {
|
||||
|
|
@ -29,9 +34,13 @@ public class AutoRecordsHolder {
|
|||
onRemoveListeners.add(onRemove);
|
||||
}
|
||||
|
||||
public Future<Void> init() {
|
||||
public synchronized Future<Void> init() {
|
||||
if (initialized) {
|
||||
return Future.succeededFuture();
|
||||
}
|
||||
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
|
||||
.onSuccess(settingAutoRecords -> {
|
||||
initialized = true;
|
||||
if (settingAutoRecords == null) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -77,4 +86,17 @@ public class AutoRecordsHolder {
|
|||
onRemoveListeners.forEach(listener -> listener.accept(removedItems));
|
||||
}
|
||||
}
|
||||
|
||||
public Future<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
|
||||
private final Map<String, TelegramVerticle> sessionTelegramVerticles = new ConcurrentHashMap<>();
|
||||
|
||||
private final AutoRecordsHolder autoRecordsHolder = new AutoRecordsHolder();
|
||||
|
||||
private final FileRouteHandler fileRouteHandler = new FileRouteHandler();
|
||||
|
||||
private static final String SESSION_COOKIE_NAME = "tf";
|
||||
|
|
@ -54,17 +52,22 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
public void start(Promise<Void> startPromise) {
|
||||
initHttpServer()
|
||||
.compose(r -> initTelegramVerticles())
|
||||
.compose(r -> autoRecordsHolder.init())
|
||||
.compose(r -> AutoRecordsHolder.INSTANCE.init())
|
||||
.compose(r -> initAutoDownloadVerticle())
|
||||
.compose(r -> initTransferVerticle())
|
||||
.compose(r -> initPreloadMessageVerticle())
|
||||
.compose(r -> initEventConsumer())
|
||||
.onSuccess(startPromise::complete)
|
||||
.onFailure(startPromise::fail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
log.info("Http verticle stopped!");
|
||||
public void stop(Promise<Void> stopPromise) {
|
||||
AutoRecordsHolder.INSTANCE.saveAutoRecords()
|
||||
.onComplete(ignore -> {
|
||||
log.info("Http verticle stopped!");
|
||||
stopPromise.complete();
|
||||
});
|
||||
}
|
||||
|
||||
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/toggle-pause-download").handler(this::handleFileTogglePauseDownload);
|
||||
router.post("/:telegramId/file/remove").handler(this::handleFileRemove);
|
||||
router.post("/:telegramId/file/auto-download").handler(this::handleAutoDownload);
|
||||
router.post("/:telegramId/file/update-auto-settings").handler(this::handleAutoSettingsUpdate);
|
||||
|
||||
router.route()
|
||||
.failureHandler(ctx -> {
|
||||
|
|
@ -182,15 +185,19 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
}
|
||||
|
||||
public Future<Void> initAutoDownloadVerticle() {
|
||||
return vertx.deployVerticle(new AutoDownloadVerticle(autoRecordsHolder), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
|
||||
return vertx.deployVerticle(new AutoDownloadVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
|
||||
.mapEmpty();
|
||||
}
|
||||
|
||||
public Future<Void> initTransferVerticle() {
|
||||
return vertx.deployVerticle(new TransferVerticle(autoRecordsHolder), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
|
||||
return vertx.deployVerticle(new TransferVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
|
||||
.mapEmpty();
|
||||
}
|
||||
|
||||
public Future<Void> initPreloadMessageVerticle() {
|
||||
return vertx.deployVerticle(new PreloadMessageVerticle(), Config.VIRTUAL_THREAD_DEPLOYMENT_OPTIONS)
|
||||
.mapEmpty();
|
||||
}
|
||||
|
||||
private Future<Void> initEventConsumer() {
|
||||
vertx.eventBus().consumer(EventEnum.TELEGRAM_EVENT.address(), message -> {
|
||||
|
|
@ -211,8 +218,8 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
});
|
||||
|
||||
vertx.eventBus().consumer(EventEnum.AUTO_DOWNLOAD_UPDATE.address(), message -> {
|
||||
log.debug("Auto download update: %s".formatted(message.body()));
|
||||
autoRecordsHolder.onAutoRecordsUpdate(Json.decodeValue(message.body().toString(), SettingAutoRecords.class));
|
||||
log.debug("Auto settings update: %s".formatted(message.body()));
|
||||
AutoRecordsHolder.INSTANCE.onAutoRecordsUpdate(Json.decodeValue(message.body().toString(), SettingAutoRecords.class));
|
||||
});
|
||||
return Future.succeededFuture();
|
||||
}
|
||||
|
|
@ -607,7 +614,7 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
.onFailure(ctx::fail);
|
||||
}
|
||||
|
||||
private void handleAutoDownload(RoutingContext ctx) {
|
||||
private void handleAutoSettingsUpdate(RoutingContext ctx) {
|
||||
TelegramVerticle telegramVerticle = TelegramVerticles.getOrElseThrow(ctx.pathParam("telegramId"));
|
||||
|
||||
String chatId = ctx.request().getParam("chatId");
|
||||
|
|
@ -616,7 +623,7 @@ public class HttpVerticle extends AbstractVerticle {
|
|||
return;
|
||||
}
|
||||
JsonObject params = ctx.body().asJsonObject();
|
||||
telegramVerticle.toggleAutoDownload(Convert.toLong(chatId), params)
|
||||
telegramVerticle.updateAutoSettings(Convert.toLong(chatId), params)
|
||||
.onSuccess(r -> ctx.end())
|
||||
.onFailure(ctx::fail);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,4 +85,51 @@ public class MessyUtils {
|
|||
default -> throw new IllegalArgumentException("Unknown unit: " + unit);
|
||||
};
|
||||
}
|
||||
|
||||
public static class BitState {
|
||||
private int state;
|
||||
|
||||
public BitState(int state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启某个状态
|
||||
*/
|
||||
public void enableState(int n) {
|
||||
state |= (1 << n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭某个状态
|
||||
*/
|
||||
public void disableState(int n) {
|
||||
state &= ~(1 << n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换某个状态(开启->关闭,关闭->开启)
|
||||
*/
|
||||
public void toggleState(int n) {
|
||||
state ^= (1 << n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查某个状态是否开启
|
||||
*/
|
||||
public boolean isStateEnabled(int n) {
|
||||
return (state & (1 << n)) != 0;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态的二进制表示
|
||||
*/
|
||||
public String getBinaryState() {
|
||||
return String.format("%8s", Integer.toBinaryString(state)).replace(' ', '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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 -> {
|
||||
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)
|
||||
.orElseThrow(() -> new NoStackTraceException("not support message type"));
|
||||
FileRecord fileRecord = fileHandler.convertFileRecord(telegramRecord.id());
|
||||
return DataVerticle.fileRepository.create(fileRecord)
|
||||
.compose(r ->
|
||||
client.execute(new TdApi.AddFileToDownloads(fileId, chatId, messageId, 32))
|
||||
)
|
||||
.onSuccess(r ->
|
||||
return DataVerticle.fileRepository.createIfNotExist(fileRecord)
|
||||
.compose(r -> {
|
||||
if (!r) {
|
||||
return DataVerticle.fileRepository.updateFileId(fileRecord.id(), fileRecord.uniqueId());
|
||||
}
|
||||
return Future.succeededFuture();
|
||||
})
|
||||
.compose(ignore -> client.execute(new TdApi.AddFileToDownloads(fileId, chatId, messageId, 32)))
|
||||
.onSuccess(ignore ->
|
||||
sendEvent(EventPayload.build(EventPayload.TYPE_FILE_STATUS, new JsonObject()
|
||||
.put("fileId", fileId)
|
||||
.put("uniqueId", fileRecord.uniqueId())
|
||||
|
|
@ -430,24 +434,33 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
.mapEmpty();
|
||||
}
|
||||
|
||||
public Future<Void> toggleAutoDownload(Long chatId, JsonObject params) {
|
||||
public Future<Void> updateAutoSettings(Long chatId, JsonObject params) {
|
||||
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
|
||||
.compose(settingAutoRecords -> {
|
||||
if (settingAutoRecords == null) {
|
||||
settingAutoRecords = new SettingAutoRecords();
|
||||
}
|
||||
if (settingAutoRecords.exists(this.telegramRecord.id(), chatId)) {
|
||||
settingAutoRecords.remove(this.telegramRecord.id(), chatId);
|
||||
} else {
|
||||
SettingAutoRecords.Rule rule = null;
|
||||
if (params != null && params.containsKey("rule")) {
|
||||
rule = params.getJsonObject("rule").mapTo(SettingAutoRecords.Rule.class);
|
||||
if (StrUtil.isBlank(rule.query) && CollUtil.isEmpty(rule.fileTypes) && rule.transferRule == null) {
|
||||
rule = null;
|
||||
}
|
||||
}
|
||||
settingAutoRecords.add(this.telegramRecord.id(), chatId, rule);
|
||||
SettingAutoRecords.Rule rule = params.getJsonObject("rule").mapTo(SettingAutoRecords.Rule.class);
|
||||
if (StrUtil.isBlank(rule.query) && CollUtil.isEmpty(rule.fileTypes) && rule.transferRule == null) {
|
||||
rule = null;
|
||||
}
|
||||
boolean downloadEnabled = params.getBoolean("downloadEnabled", false);
|
||||
boolean preloadEnabled = params.getBoolean("preloadEnabled", false);
|
||||
|
||||
SettingAutoRecords.Item item = settingAutoRecords.getItem(this.telegramRecord.id(), chatId);
|
||||
if (downloadEnabled || preloadEnabled) {
|
||||
if (item == null) {
|
||||
item = new SettingAutoRecords.Item(this.telegramRecord.id(), chatId, rule);
|
||||
}
|
||||
item.downloadEnabled = downloadEnabled;
|
||||
item.preloadEnabled = preloadEnabled;
|
||||
item.rule = downloadEnabled ? rule : null;
|
||||
|
||||
settingAutoRecords.add(item);
|
||||
} else if (item != null) {
|
||||
settingAutoRecords.remove(this.telegramRecord.id(), chatId);
|
||||
}
|
||||
|
||||
return DataVerticle.settingRepository.createOrUpdate(SettingKey.autoDownload.name(), Json.encode(settingAutoRecords));
|
||||
})
|
||||
.onSuccess(r -> vertx.eventBus().publish(EventEnum.AUTO_DOWNLOAD_UPDATE.name(), r.value()))
|
||||
|
|
@ -807,25 +820,22 @@ public class TelegramVerticle extends AbstractVerticle {
|
|||
//<-----------------------------convert---------------------------------->
|
||||
|
||||
private Future<JsonArray> convertChat(List<TdApi.Chat> chats) {
|
||||
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
|
||||
.map(settingAutoRecords -> settingAutoRecords == null ? new HashMap<Long, SettingAutoRecords.Item>()
|
||||
: settingAutoRecords.getItems(this.telegramRecord.id()))
|
||||
.map(enableAutoChats -> new JsonArray(chats.stream()
|
||||
.map(chat -> {
|
||||
SettingAutoRecords.Item autoItem = enableAutoChats.get(chat.id);
|
||||
return new JsonObject()
|
||||
.put("id", Convert.toStr(chat.id))
|
||||
.put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title)
|
||||
.put("type", TdApiHelp.getChatType(chat.type))
|
||||
.put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(chat, "photo.minithumbnail.data")))
|
||||
.put("unreadCount", chat.unreadCount)
|
||||
.put("lastMessage", "")
|
||||
.put("lastMessageTime", "")
|
||||
.put("autoEnabled", autoItem != null)
|
||||
.put("autoRule", autoItem == null ? null : autoItem.rule);
|
||||
})
|
||||
.toList()
|
||||
));
|
||||
Map<Long, SettingAutoRecords.Item> enableAutoChats = AutoRecordsHolder.INSTANCE.autoRecords().getItems(this.telegramRecord.id());
|
||||
return Future.succeededFuture(new JsonArray(chats.stream()
|
||||
.map(chat -> {
|
||||
SettingAutoRecords.Item auto = enableAutoChats.get(chat.id);
|
||||
return new JsonObject()
|
||||
.put("id", Convert.toStr(chat.id))
|
||||
.put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title)
|
||||
.put("type", TdApiHelp.getChatType(chat.type))
|
||||
.put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(chat, "photo.minithumbnail.data")))
|
||||
.put("unreadCount", chat.unreadCount)
|
||||
.put("lastMessage", "")
|
||||
.put("lastMessageTime", "")
|
||||
.put("auto", auto);
|
||||
})
|
||||
.toList()
|
||||
));
|
||||
}
|
||||
|
||||
private Future<JsonObject> convertFiles(Tuple2<TdApi.FoundChatMessages, Map<String, FileRecord>> tuple) {
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ public class TransferVerticle extends AbstractVerticle {
|
|||
|
||||
private volatile Transfer beingTransferred;
|
||||
|
||||
public TransferVerticle(AutoRecordsHolder autoRecordsHolder) {
|
||||
this.autoRecords = autoRecordsHolder.autoRecords();
|
||||
autoRecordsHolder.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> {
|
||||
public TransferVerticle() {
|
||||
this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords();
|
||||
AutoRecordsHolder.INSTANCE.registerOnRemoveListener(removedItems -> removedItems.forEach(item -> {
|
||||
waitingTransferFiles.removeIf(waitingTransferFile -> waitingTransferFile.uniqueId().equals(item.uniqueKey()));
|
||||
transfers.remove(item.uniqueKey());
|
||||
}));
|
||||
|
|
@ -109,7 +109,7 @@ public class TransferVerticle extends AbstractVerticle {
|
|||
log.debug("Start scan history files for transfer");
|
||||
for (SettingAutoRecords.Item item : autoRecords.items) {
|
||||
Transfer transfer = getTransfer(item);
|
||||
if (transfer == null || !transfer.transferHistory) {
|
||||
if (transfer == null || !transfer.transferHistory || item.isNotComplete(SettingAutoRecords.HISTORY_PRELOAD_STATE)) {
|
||||
continue;
|
||||
}
|
||||
Tuple3<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;
|
||||
if (CollUtil.isEmpty(files)) {
|
||||
log.debug("No history files found for transfer: %s".formatted(item.uniqueKey()));
|
||||
item.complete(SettingAutoRecords.HISTORY_TRANSFER_STATE);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import java.util.Map;
|
|||
public interface FileRepository {
|
||||
Future<FileRecord> create(FileRecord fileRecord);
|
||||
|
||||
Future<Boolean> createIfNotExist(FileRecord fileRecord);
|
||||
|
||||
Future<Map<Integer, FileRecord>> getFiles(long chatId, List<Integer> fileIds);
|
||||
|
||||
Future<Tuple3<List<FileRecord>, Long, Long>> getFiles(long chatId, Map<String, String> filter);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package telegram.files.repository;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import telegram.files.MessyUtils;
|
||||
import telegram.files.Transfer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -12,6 +14,12 @@ public class SettingAutoRecords {
|
|||
|
||||
public List<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 long telegramId;
|
||||
|
||||
|
|
@ -23,7 +31,17 @@ public class SettingAutoRecords {
|
|||
|
||||
public Rule rule;
|
||||
|
||||
public boolean downloadEnabled;
|
||||
|
||||
public boolean preloadEnabled;
|
||||
|
||||
public long nextFromMessageIdForPreload;
|
||||
|
||||
public int state;
|
||||
|
||||
public Item() {
|
||||
// downloadEnabled default is true
|
||||
this.downloadEnabled = true;
|
||||
}
|
||||
|
||||
public Item(long telegramId, long chatId, Rule rule) {
|
||||
|
|
@ -35,6 +53,17 @@ public class SettingAutoRecords {
|
|||
public String uniqueKey() {
|
||||
return telegramId + ":" + chatId;
|
||||
}
|
||||
|
||||
public void complete(int bitwise) {
|
||||
MessyUtils.BitState bitState = new MessyUtils.BitState(state);
|
||||
bitState.enableState(bitwise);
|
||||
state = bitState.getState();
|
||||
}
|
||||
|
||||
public boolean isNotComplete(int bitwise) {
|
||||
MessyUtils.BitState bitState = new MessyUtils.BitState(state);
|
||||
return !bitState.isStateEnabled(bitwise);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Rule {
|
||||
|
|
@ -102,6 +131,20 @@ public class SettingAutoRecords {
|
|||
items.removeIf(item -> item.telegramId == telegramId && item.chatId == chatId);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public List<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) {
|
||||
return items.stream()
|
||||
.filter(item -> item.telegramId == telegramId)
|
||||
|
|
|
|||
|
|
@ -57,11 +57,19 @@ public class FileRepositoryImpl implements FileRepository {
|
|||
.execute(fileRecord)
|
||||
.map(r -> fileRecord)
|
||||
.compose(r -> this.updateCaptionByMediaAlbumId(fileRecord.mediaAlbumId(), fileRecord.caption()).map(r))
|
||||
.onSuccess(r -> log.trace("Successfully created file record: %s".formatted(fileRecord.id()))
|
||||
)
|
||||
.onFailure(
|
||||
err -> log.error("Failed to create file record: %s".formatted(err.getMessage()))
|
||||
);
|
||||
.onSuccess(r -> log.trace("Successfully created file record: %s".formatted(fileRecord.id())))
|
||||
.onFailure(err -> log.error("Failed to create file record: %s".formatted(err.getMessage())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<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
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class AutoRecordsHolderTest {
|
|||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
autoRecordsHolder = new AutoRecordsHolder();
|
||||
autoRecordsHolder = AutoRecordsHolder.INSTANCE;
|
||||
telegramVerticlesMockedStatic = mockStatic(TelegramVerticles.class);
|
||||
|
||||
// Prepare test data
|
||||
|
|
|
|||
|
|
@ -8,16 +8,19 @@ import {
|
|||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { type TelegramChat } from "@/lib/types";
|
||||
|
||||
interface AutoDownloadButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
autoEnabled?: boolean;
|
||||
auto?: TelegramChat["auto"];
|
||||
}
|
||||
|
||||
const AutoDownloadButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
AutoDownloadButtonProps
|
||||
>(({ autoEnabled = false, className, ...props }, ref) => {
|
||||
>(({ auto, className, ...props }, ref) => {
|
||||
const autoEnabled = auto && (auto.downloadEnabled || auto.preloadEnabled);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
|
|
@ -40,15 +43,19 @@ const AutoDownloadButton = React.forwardRef<
|
|||
<div
|
||||
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",
|
||||
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>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{autoEnabled
|
||||
? "Auto download is enabled, you can disable it by clicking the button"
|
||||
: "Auto download is disabled, you can enable it by clicking the button"}
|
||||
? "Automation is enabled, you can disable it by clicking the button"
|
||||
: "Automation is disabled, you can enable it by clicking the button"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import useSWRMutation from "swr/mutation";
|
||||
import { POST } from "@/lib/api";
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
|
@ -55,6 +55,9 @@ export default function AutoDownloadDialog() {
|
|||
const { isLoading, chat, reload } = useTelegramChat();
|
||||
const { toast } = useToast();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [downloadEnabled, setDownloadEnabled] = useState(false);
|
||||
const [preloadEnabled, setPreloadEnabled] = useState(false);
|
||||
const [rule, setRule] = useState<AutoDownloadRule>({
|
||||
query: "",
|
||||
fileTypes: [],
|
||||
|
|
@ -62,18 +65,28 @@ export default function AutoDownloadDialog() {
|
|||
const { trigger: triggerAuto, isMutating: isAutoMutating } = useSWRMutation(
|
||||
!accountId || !chat
|
||||
? undefined
|
||||
: `/${accountId}/file/auto-download?telegramId=${accountId}&chatId=${chat?.id}`,
|
||||
(key, { arg }: { arg: { rule: AutoDownloadRule } }) => {
|
||||
: `/${accountId}/file/update-auto-settings?telegramId=${accountId}&chatId=${chat?.id}`,
|
||||
(
|
||||
key,
|
||||
{
|
||||
arg,
|
||||
}: {
|
||||
arg: {
|
||||
downloadEnabled: boolean;
|
||||
preloadEnabled: boolean;
|
||||
rule: AutoDownloadRule;
|
||||
};
|
||||
},
|
||||
) => {
|
||||
return POST(key, arg);
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: chat?.autoEnabled
|
||||
? "Auto download disabled for this chat!"
|
||||
: "Auto download enabled for this chat!",
|
||||
title: "Auto settings updated!",
|
||||
});
|
||||
void reload();
|
||||
setEditMode(false);
|
||||
setTimeout(() => {
|
||||
setOpen(false);
|
||||
}, 1000);
|
||||
|
|
@ -85,6 +98,18 @@ export default function AutoDownloadDialog() {
|
|||
leading: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (chat?.auto) {
|
||||
setDownloadEnabled(chat.auto.downloadEnabled);
|
||||
setPreloadEnabled(chat.auto.preloadEnabled);
|
||||
setRule(chat.auto.rule ?? { query: "", fileTypes: [] });
|
||||
} else {
|
||||
setDownloadEnabled(false);
|
||||
setPreloadEnabled(false);
|
||||
setRule({ query: "", fileTypes: [] });
|
||||
}
|
||||
}, [chat]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
{chat && <AutoDownloadButton autoEnabled={chat.autoEnabled} />}
|
||||
{chat && <AutoDownloadButton auto={chat.auto} />}
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
aria-describedby={undefined}
|
||||
onPointerDownOutside={() => setOpen(false)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-full w-full overflow-auto md:h-auto md:max-h-[85%] md:w-auto"
|
||||
className="h-full w-full overflow-auto md:h-auto md:max-h-[85%] md:min-w-[400px]"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{chat?.autoEnabled
|
||||
? "Disable Auto Download"
|
||||
: "Enable auto download for this chat?"}
|
||||
Update Auto Settings for {chat?.name ?? "Unknown Chat"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription></DialogDescription>
|
||||
{chat?.autoEnabled ? (
|
||||
{!editMode && chat?.auto ? (
|
||||
<div className="space-y-4">
|
||||
{chat.autoRule && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center">
|
||||
<Label className="text-sm font-semibold text-gray-900 dark:text-gray-300">
|
||||
Current Rule
|
||||
</Label>
|
||||
<Badge
|
||||
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"
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Filter Keyword Section */}
|
||||
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<span className="text-xs font-medium text-gray-500">
|
||||
Filter Keyword
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-300">
|
||||
{chat.autoRule.query || "No keyword specified"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<span className="text-xs font-medium text-gray-500">
|
||||
File Types
|
||||
</span>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{chat.autoRule.fileTypes.length > 0 ? (
|
||||
chat.autoRule.fileTypes.map((type) => (
|
||||
<Badge
|
||||
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"
|
||||
>
|
||||
{type}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-300">
|
||||
No file types selected
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transfer Rule Section */}
|
||||
{chat.autoRule.transferRule && (
|
||||
<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 preload
|
||||
</Label>
|
||||
<Badge
|
||||
variant="outline"
|
||||
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",
|
||||
)}
|
||||
>
|
||||
{chat.auto.preloadEnabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</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">
|
||||
{/* Filter Keyword Section */}
|
||||
<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">
|
||||
Transfer Rule
|
||||
Filter Keyword
|
||||
</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.autoRule.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.autoRule.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.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"}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-300">
|
||||
{chat.auto.rule.query || "No keyword specified"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<span className="text-xs font-medium text-gray-500">
|
||||
File Types
|
||||
</span>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{chat.auto.rule.fileTypes.length > 0 ? (
|
||||
chat.auto.rule.fileTypes.map((type) => (
|
||||
<Badge
|
||||
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"
|
||||
>
|
||||
{type}
|
||||
</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 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 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="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 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="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>
|
||||
<AutoDownloadRule value={rule} onChange={setRule} />
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={debounceIsAutoMutating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const folderPathRegex = /^[\/\\]?(?:[^<>:"|?*\/\\]+[\/\\]?)*$/;
|
||||
if (
|
||||
rule.transferRule &&
|
||||
!folderPathRegex.test(rule.transferRule.destination)
|
||||
) {
|
||||
toast({
|
||||
title: "Invalid destination folder",
|
||||
description: "Please enter a valid destination folder path",
|
||||
});
|
||||
return;
|
||||
}
|
||||
void triggerAuto({ rule });
|
||||
}}
|
||||
variant={chat?.autoEnabled ? "destructive" : "default"}
|
||||
disabled={debounceIsAutoMutating}
|
||||
>
|
||||
{debounceIsAutoMutating
|
||||
? "Submitting..."
|
||||
: chat?.autoEnabled
|
||||
? "Disable"
|
||||
: "Enable"}
|
||||
</Button>
|
||||
{!editMode && chat?.auto ? (
|
||||
<Button variant="outline" onClick={() => setEditMode(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={debounceIsAutoMutating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const folderPathRegex =
|
||||
/^[\/\\]?(?:[^<>:"|?*\/\\]+[\/\\]?)*$/;
|
||||
if (
|
||||
rule.transferRule &&
|
||||
!folderPathRegex.test(rule.transferRule.destination)
|
||||
) {
|
||||
toast({
|
||||
title: "Invalid destination folder",
|
||||
description:
|
||||
"Please enter a valid destination folder path",
|
||||
});
|
||||
return;
|
||||
}
|
||||
void triggerAuto({ downloadEnabled, preloadEnabled, rule });
|
||||
}}
|
||||
disabled={debounceIsAutoMutating}
|
||||
>
|
||||
{debounceIsAutoMutating ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
@ -546,11 +638,13 @@ const PolicyLegends: Record<
|
|||
},
|
||||
OVERWRITE: {
|
||||
title: "Overwrite",
|
||||
description: "If destination exists same name file, move and overwrite the file.",
|
||||
description:
|
||||
"If destination exists same name file, move and overwrite the file.",
|
||||
},
|
||||
SKIP: {
|
||||
title: "Skip",
|
||||
description: "If destination exists same name file, skip the file, nothing to do.",
|
||||
description:
|
||||
"If destination exists same name file, skip the file, nothing to do.",
|
||||
},
|
||||
RENAME: {
|
||||
title: "Rename",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import useSWR from "swr";
|
|||
import { Ellipsis } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface FileTypeFilterProps {
|
||||
offline: boolean;
|
||||
|
|
@ -55,6 +56,13 @@ export default function FileTypeFilter({
|
|||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!offline && localType === "all") {
|
||||
setLocalType("media");
|
||||
onChange("media");
|
||||
}
|
||||
}, [localType, offline, onChange]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label>Type</Label>
|
||||
|
|
@ -63,7 +71,7 @@ export default function FileTypeFilter({
|
|||
<SelectValue placeholder="File type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{offline && <SelectItem value="all">All files</SelectItem>}
|
||||
{offline && <SelectItem value="all">All Files</SelectItem>}
|
||||
<FileTypeSelectItem value="media" />
|
||||
<FileTypeSelectItem value="photo" />
|
||||
<FileTypeSelectItem value="video" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function TelegramIcon({ className }: { className?: string }) {
|
|||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn(className, "dark:fill-current")}
|
||||
className={cn(className, "dark:fill-white")}
|
||||
>
|
||||
<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" />
|
||||
|
|
|
|||
|
|
@ -19,8 +19,12 @@ export type TelegramChat = {
|
|||
unreadCount?: number;
|
||||
lastMessage?: string;
|
||||
lastMessageTime?: string;
|
||||
autoEnabled: boolean;
|
||||
autoRule?: AutoDownloadRule;
|
||||
auto?: {
|
||||
preloadEnabled: boolean;
|
||||
downloadEnabled: boolean;
|
||||
rule?: AutoDownloadRule;
|
||||
state: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type FileType = "media" | "photo" | "video" | "audio" | "file";
|
||||
|
|
|
|||
Loading…
Reference in a new issue