feat: Support preload file messages.

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

View file

@ -8,7 +8,6 @@ import cn.hutool.log.LogFactory;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.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))

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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');
}
}
}

View file

@ -0,0 +1,120 @@
package telegram.files;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import org.drinkless.tdlib.TdApi;
import telegram.files.repository.FileRecord;
import telegram.files.repository.SettingAutoRecords;
import java.util.Optional;
public class PreloadMessageVerticle extends AbstractVerticle {
private static final Log log = LogFactory.get();
private static final int HISTORY_SCAN_INTERVAL = 30 * 1000;
private static final int MAX_HISTORY_SCAN_TIME = 10 * 1000;
private final SettingAutoRecords autoRecords;
public PreloadMessageVerticle() {
this.autoRecords = AutoRecordsHolder.INSTANCE.autoRecords();
}
@Override
public void start(Promise<Void> startPromise) {
initEventConsumer()
.onSuccess(r -> {
vertx.setPeriodic(0, HISTORY_SCAN_INTERVAL,
id -> autoRecords.getPreloadEnabledItems()
.stream()
.filter(auto -> auto.isNotComplete(SettingAutoRecords.HISTORY_PRELOAD_STATE))
.forEach(auto -> addHistoryMessage(auto, System.currentTimeMillis())));
log.info("""
Preload message verticle started!
|Auto chats: %s
""".formatted(autoRecords.getPreloadEnabledItems().size()));
startPromise.complete();
})
.onFailure(startPromise::fail);
}
@Override
public void stop() {
log.info("Preload message verticle stopped!");
}
private Future<Void> initEventConsumer() {
vertx.eventBus().consumer(EventEnum.MESSAGE_RECEIVED.address(), message -> {
log.trace("Auto download message received: %s".formatted(message.body()));
this.onNewMessage((JsonObject) message.body());
});
return Future.succeededFuture();
}
private void addHistoryMessage(SettingAutoRecords.Item auto, long currentTimeMillis) {
log.debug("Start load history message! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId));
if (System.currentTimeMillis() - currentTimeMillis > MAX_HISTORY_SCAN_TIME) {
log.debug("Load history message timeout! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId));
return;
}
TelegramVerticle telegramVerticle = TelegramVerticles.getOrElseThrow(auto.telegramId);
TdApi.SearchChatMessages searchChatMessages = new TdApi.SearchChatMessages();
searchChatMessages.chatId = auto.chatId;
searchChatMessages.fromMessageId = auto.nextFromMessageIdForPreload;
searchChatMessages.limit = 100;
TdApi.FoundChatMessages foundChatMessages = Future.await(telegramVerticle.client.execute(searchChatMessages)
.onFailure(r -> log.error("Search chat messages failed! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId), r))
);
if (foundChatMessages == null || foundChatMessages.messages.length == 0) {
log.debug("%s No more history message found! TelegramId: %d ChatId: %d".formatted(auto.uniqueKey(), auto.telegramId, auto.chatId));
auto.complete(SettingAutoRecords.HISTORY_PRELOAD_STATE);
return;
}
int count = 0;
for (TdApi.Message message : foundChatMessages.messages) {
Optional<TdApiHelp.FileHandler<? extends TdApi.MessageContent>> fileHandlerOptional = TdApiHelp.getFileHandler(message);
if (fileHandlerOptional.isEmpty()) {
continue;
}
FileRecord fileRecord = fileHandlerOptional.get().convertFileRecord(auto.telegramId);
if (Future.await(DataVerticle.fileRepository.createIfNotExist(fileRecord))) {
count++;
}
}
if (log.isDebugEnabled() && count > 0) {
log.debug("Load history message success! TelegramId: %d ChatId: %d Count: %d".formatted(auto.telegramId, auto.chatId, count));
}
auto.nextFromMessageIdForPreload = foundChatMessages.nextFromMessageId;
addHistoryMessage(auto, currentTimeMillis);
}
private void onNewMessage(JsonObject jsonObject) {
long telegramId = jsonObject.getLong("telegramId");
long chatId = jsonObject.getLong("chatId");
long messageId = jsonObject.getLong("messageId");
autoRecords.getPreloadEnabledItems().stream()
.filter(item -> item.telegramId == telegramId && item.chatId == chatId)
.findFirst()
.flatMap(ignore -> TelegramVerticles.get(telegramId))
.ifPresent(telegramVerticle -> {
if (!telegramVerticle.authorized) return;
telegramVerticle.client.execute(new TdApi.GetMessage(chatId, messageId))
.onSuccess(message -> TdApiHelp.getFileHandler(message).ifPresent(fileHandler -> {
FileRecord fileRecord = fileHandler.convertFileRecord(telegramId);
DataVerticle.fileRepository.createIfNotExist(fileRecord);
}))
.onFailure(e -> log.error("Preload message fail. Get message failed: %s".formatted(e.getMessage())));
});
}
}

View file

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

View file

@ -312,11 +312,15 @@ public class TelegramVerticle extends AbstractVerticle {
TdApiHelp.FileHandler<? extends TdApi.MessageContent> fileHandler = TdApiHelp.getFileHandler(message)
.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) {

View file

@ -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;
}

View file

@ -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);

View file

@ -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)

View file

@ -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

View file

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

View file

@ -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>

View file

@ -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",

View file

@ -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" />

View file

@ -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" />

View file

@ -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";