feat: Add automatic download rule settings.

This commit is contained in:
jarvis2f 2025-01-07 16:30:23 +08:00
parent 88f65da57b
commit 0c5016e3f3
11 changed files with 716 additions and 67 deletions

View file

@ -11,6 +11,7 @@ import io.vertx.core.Promise;
import io.vertx.core.json.Json; import io.vertx.core.json.Json;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import org.drinkless.tdlib.TdApi; import org.drinkless.tdlib.TdApi;
import org.jooq.lambda.tuple.Tuple2;
import telegram.files.repository.FileRecord; import telegram.files.repository.FileRecord;
import telegram.files.repository.SettingAutoRecords; import telegram.files.repository.SettingAutoRecords;
import telegram.files.repository.SettingKey; import telegram.files.repository.SettingKey;
@ -34,7 +35,7 @@ public class AutoDownloadVerticle extends AbstractVerticle {
private static final int DOWNLOAD_INTERVAL = 10 * 1000; private static final int DOWNLOAD_INTERVAL = 10 * 1000;
private static final List<String> FILE_TYPE_ORDER = List.of("photo", "video", "audio", "file"); private static final List<String> DEFAULT_FILE_TYPE_ORDER = List.of("photo", "video", "audio", "file");
// telegramId -> messages // telegramId -> messages
private final Map<Long, LinkedList<TdApi.Message>> waitingDownloadMessages = new ConcurrentHashMap<>(); private final Map<Long, LinkedList<TdApi.Message>> waitingDownloadMessages = new ConcurrentHashMap<>();
@ -137,11 +138,11 @@ public class AutoDownloadVerticle extends AbstractVerticle {
log.debug("Scan history end! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId)); log.debug("Scan history end! TelegramId: %d ChatId: %d".formatted(auto.telegramId, auto.chatId));
return; return;
} }
if (StrUtil.isBlank(auto.nextFileType)) { Tuple2<String, List<String>> rule = handleRule(auto);
auto.nextFileType = FILE_TYPE_ORDER.getFirst();
}
TelegramVerticle telegramVerticle = this.getTelegramVerticle(auto.telegramId); TelegramVerticle telegramVerticle = this.getTelegramVerticle(auto.telegramId);
TdApi.SearchChatMessages searchChatMessages = new TdApi.SearchChatMessages(); TdApi.SearchChatMessages searchChatMessages = new TdApi.SearchChatMessages();
searchChatMessages.query = rule.v1;
searchChatMessages.chatId = auto.chatId; searchChatMessages.chatId = auto.chatId;
searchChatMessages.fromMessageId = auto.nextFromMessageId; searchChatMessages.fromMessageId = auto.nextFromMessageId;
searchChatMessages.limit = Math.min(MAX_WAITING_LENGTH, 100); searchChatMessages.limit = Math.min(MAX_WAITING_LENGTH, 100);
@ -153,10 +154,11 @@ public class AutoDownloadVerticle extends AbstractVerticle {
return; return;
} }
if (foundChatMessages.messages.length == 0) { if (foundChatMessages.messages.length == 0) {
int nextTypeIndex = FILE_TYPE_ORDER.indexOf(auto.nextFileType) + 1; List<String> fileTypes = rule.v2;
if (nextTypeIndex < FILE_TYPE_ORDER.size()) { int nextTypeIndex = fileTypes.indexOf(auto.nextFileType) + 1;
if (nextTypeIndex < fileTypes.size()) {
String originalType = auto.nextFileType; String originalType = auto.nextFileType;
auto.nextFileType = FILE_TYPE_ORDER.get(nextTypeIndex); auto.nextFileType = fileTypes.get(nextTypeIndex);
auto.nextFromMessageId = 0; auto.nextFromMessageId = 0;
log.debug("%s No more %s files found! Switch to %s".formatted(auto.uniqueKey(), originalType, auto.nextFileType)); log.debug("%s No more %s files found! Switch to %s".formatted(auto.uniqueKey(), originalType, auto.nextFileType));
addHistoryMessage(auto, currentTimeMillis); addHistoryMessage(auto, currentTimeMillis);
@ -181,6 +183,25 @@ public class AutoDownloadVerticle extends AbstractVerticle {
} }
} }
private Tuple2<String, List<String>> handleRule(SettingAutoRecords.Item auto) {
SettingAutoRecords.Rule rule = auto.rule;
String query = null;
List<String> fileTypes = DEFAULT_FILE_TYPE_ORDER;
if (rule != null) {
if (StrUtil.isNotBlank(rule.query)) {
query = rule.query;
}
if (CollUtil.isNotEmpty(rule.fileTypes)) {
fileTypes = rule.fileTypes;
}
}
if (StrUtil.isBlank(auto.nextFileType)) {
auto.nextFileType = fileTypes.getFirst();
}
return new Tuple2<>(query, fileTypes);
}
private boolean isExceedLimit(long telegramId) { private boolean isExceedLimit(long telegramId) {
List<TdApi.Message> waitingMessages = this.waitingDownloadMessages.get(telegramId); List<TdApi.Message> waitingMessages = this.waitingDownloadMessages.get(telegramId);
return getSurplusSize(telegramId) <= 0 || (waitingMessages != null && waitingMessages.size() > limit); return getSurplusSize(telegramId) <= 0 || (waitingMessages != null && waitingMessages.size() > limit);

View file

@ -605,8 +605,8 @@ public class HttpVerticle extends AbstractVerticle {
ctx.fail(400); ctx.fail(400);
return; return;
} }
JsonObject params = ctx.body().asJsonObject();
telegramVerticle.toggleAutoDownload(Convert.toLong(chatId)) telegramVerticle.toggleAutoDownload(Convert.toLong(chatId), params)
.onSuccess(r -> ctx.end()) .onSuccess(r -> ctx.end())
.onFailure(ctx::fail); .onFailure(ctx::fail);
} }

View file

@ -3,6 +3,7 @@ package telegram.files;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64; import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateField; import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DatePattern;
@ -439,7 +440,7 @@ public class TelegramVerticle extends AbstractVerticle {
.mapEmpty(); .mapEmpty();
} }
public Future<Void> toggleAutoDownload(Long chatId) { public Future<Void> toggleAutoDownload(Long chatId, JsonObject params) {
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload) return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
.compose(settingAutoRecords -> { .compose(settingAutoRecords -> {
if (settingAutoRecords == null) { if (settingAutoRecords == null) {
@ -448,7 +449,14 @@ public class TelegramVerticle extends AbstractVerticle {
if (settingAutoRecords.exists(this.telegramRecord.id(), chatId)) { if (settingAutoRecords.exists(this.telegramRecord.id(), chatId)) {
settingAutoRecords.remove(this.telegramRecord.id(), chatId); settingAutoRecords.remove(this.telegramRecord.id(), chatId);
} else { } else {
settingAutoRecords.add(this.telegramRecord.id(), chatId); 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 = null;
}
}
settingAutoRecords.add(this.telegramRecord.id(), chatId, rule);
} }
return DataVerticle.settingRepository.createOrUpdate(SettingKey.autoDownload.name(), Json.encode(settingAutoRecords)); return DataVerticle.settingRepository.createOrUpdate(SettingKey.autoDownload.name(), Json.encode(settingAutoRecords));
}) })
@ -824,10 +832,12 @@ public class TelegramVerticle extends AbstractVerticle {
private Future<JsonArray> convertChat(List<TdApi.Chat> chats) { private Future<JsonArray> convertChat(List<TdApi.Chat> chats) {
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload) return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
.map(settingAutoRecords -> settingAutoRecords == null ? Collections.emptySet() .map(settingAutoRecords -> settingAutoRecords == null ? new HashMap<Long, SettingAutoRecords.Item>()
: settingAutoRecords.getChatIds(this.telegramRecord.id())) : settingAutoRecords.getItems(this.telegramRecord.id()))
.map(enableAutoChatIds -> new JsonArray(chats.stream() .map(enableAutoChats -> new JsonArray(chats.stream()
.map(chat -> new JsonObject() .map(chat -> {
SettingAutoRecords.Item autoItem = enableAutoChats.get(chat.id);
return new JsonObject()
.put("id", Convert.toStr(chat.id)) .put("id", Convert.toStr(chat.id))
.put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title) .put("name", chat.id == this.telegramRecord.id() ? "Saved Messages" : chat.title)
.put("type", TdApiHelp.getChatType(chat.type)) .put("type", TdApiHelp.getChatType(chat.type))
@ -835,8 +845,9 @@ public class TelegramVerticle extends AbstractVerticle {
.put("unreadCount", chat.unreadCount) .put("unreadCount", chat.unreadCount)
.put("lastMessage", "") .put("lastMessage", "")
.put("lastMessageTime", "") .put("lastMessageTime", "")
.put("autoEnabled", enableAutoChatIds.contains(chat.id)) .put("autoEnabled", autoItem != null)
) .put("autoRule", autoItem == null ? null : autoItem.rule);
})
.toList() .toList()
)); ));
} }

View file

@ -2,7 +2,8 @@ package telegram.files.repository;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class SettingAutoRecords { public class SettingAutoRecords {
@ -18,12 +19,15 @@ public class SettingAutoRecords {
public long nextFromMessageId; public long nextFromMessageId;
public Rule rule;
public Item() { public Item() {
} }
public Item(long telegramId, long chatId) { public Item(long telegramId, long chatId, Rule rule) {
this.telegramId = telegramId; this.telegramId = telegramId;
this.chatId = chatId; this.chatId = chatId;
this.rule = rule;
} }
public String uniqueKey() { public String uniqueKey() {
@ -31,6 +35,20 @@ public class SettingAutoRecords {
} }
} }
public static class Rule {
public String query;
public List<String> fileTypes;
public Rule() {
}
public Rule(String query, List<String> fileTypes) {
this.query = query;
this.fileTypes = fileTypes;
}
}
public SettingAutoRecords() { public SettingAutoRecords() {
this.items = new ArrayList<>(); this.items = new ArrayList<>();
} }
@ -48,19 +66,18 @@ public class SettingAutoRecords {
items.add(item); items.add(item);
} }
public void add(long telegramId, long chatId) { public void add(long telegramId, long chatId, Rule rule) {
items.add(new Item(telegramId, chatId)); items.add(new Item(telegramId, chatId, rule));
} }
public void remove(long telegramId, long chatId) { public void remove(long telegramId, long chatId) {
items.removeIf(item -> item.telegramId == telegramId && item.chatId == chatId); items.removeIf(item -> item.telegramId == telegramId && item.chatId == chatId);
} }
public Set<Long> getChatIds(long telegramId) { public Map<Long, Item> getItems(long telegramId) {
return items.stream() return items.stream()
.filter(item -> item.telegramId == telegramId) .filter(item -> item.telegramId == telegramId)
.map(item -> item.chatId) .collect(Collectors.toMap(i -> i.chatId, Function.identity()));
.collect(Collectors.toSet());
} }
} }

333
web/package-lock.json generated
View file

@ -12,6 +12,7 @@
"@dnd-kit/modifiers": "^8.0.0", "@dnd-kit/modifiers": "^8.0.0",
"@dnd-kit/sortable": "^9.0.0", "@dnd-kit/sortable": "^9.0.0",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dialog": "^1.1.4",
@ -970,6 +971,119 @@
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
}, },
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz",
"integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-collection": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": { "node_modules/@radix-ui/react-arrow": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
@ -1046,6 +1160,116 @@
} }
} }
}, },
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
"integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": { "node_modules/@radix-ui/react-collection": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
@ -8322,6 +8546,62 @@
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
}, },
"@radix-ui/react-accordion": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz",
"integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==",
"requires": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-collection": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"dependencies": {
"@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
},
"@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
"requires": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1"
}
},
"@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"requires": {}
},
"@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"requires": {
"@radix-ui/react-slot": "1.1.1"
}
},
"@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"requires": {
"@radix-ui/react-compose-refs": "1.1.1"
}
}
}
},
"@radix-ui/react-arrow": { "@radix-ui/react-arrow": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
@ -8356,6 +8636,59 @@
"@radix-ui/react-use-size": "1.1.0" "@radix-ui/react-use-size": "1.1.0"
} }
}, },
"@radix-ui/react-collapsible": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
"integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==",
"requires": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"dependencies": {
"@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
},
"@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"requires": {}
},
"@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"requires": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
}
},
"@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"requires": {
"@radix-ui/react-slot": "1.1.1"
}
},
"@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"requires": {
"@radix-ui/react-compose-refs": "1.1.1"
}
}
}
},
"@radix-ui/react-collection": { "@radix-ui/react-collection": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",

View file

@ -20,6 +20,7 @@
"@dnd-kit/modifiers": "^8.0.0", "@dnd-kit/modifiers": "^8.0.0",
"@dnd-kit/sortable": "^9.0.0", "@dnd-kit/sortable": "^9.0.0",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dialog": "^1.1.4",

View file

@ -7,7 +7,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useState } from "react"; import React, { useState } from "react";
import useSWRMutation from "swr/mutation"; import useSWRMutation from "swr/mutation";
import { POST } from "@/lib/api"; import { POST } from "@/lib/api";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
@ -15,17 +15,41 @@ import { useToast } from "@/hooks/use-toast";
import { AutoDownloadButton } from "@/components/auto-download-button"; import { AutoDownloadButton } from "@/components/auto-download-button";
import { useTelegramChat } from "@/hooks/use-telegram-chat"; import { useTelegramChat } from "@/hooks/use-telegram-chat";
import { useTelegramAccount } from "@/hooks/use-telegram-account"; import { useTelegramAccount } from "@/hooks/use-telegram-account";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { type AutoDownloadRule, type FileType } from "@/lib/types";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Badge } from "./ui/badge";
import { X } from "lucide-react";
export default function AutoDownloadDialog() { export default function AutoDownloadDialog() {
const { accountId } = useTelegramAccount(); const { accountId } = useTelegramAccount();
const { isLoading, chat, reload } = useTelegramChat(); const { isLoading, chat, reload } = useTelegramChat();
const { toast } = useToast(); const { toast } = useToast();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [rule, setRule] = useState<AutoDownloadRule>({
query: "",
fileTypes: [],
});
const { trigger: triggerAuto, isMutating: isAutoMutating } = useSWRMutation( const { trigger: triggerAuto, isMutating: isAutoMutating } = useSWRMutation(
!accountId || !chat !accountId || !chat
? undefined ? undefined
: `/file/auto-download?telegramId=${accountId}&chatId=${chat?.id}`, : `/file/auto-download?telegramId=${accountId}&chatId=${chat?.id}`,
POST, (key, { arg }: { arg: { rule: AutoDownloadRule } }) => {
return POST(key, arg);
},
{ {
onSuccess: () => { onSuccess: () => {
toast({ toast({
@ -74,23 +98,78 @@ export default function AutoDownloadDialog() {
</DialogHeader> </DialogHeader>
<DialogDescription></DialogDescription> <DialogDescription></DialogDescription>
{chat?.autoEnabled ? ( {chat?.autoEnabled ? (
<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">
Current Rule
</Label>
<Badge
variant="outline"
className="ml-2 border-blue-200 bg-blue-50 px-2 py-0.5 text-xs text-blue-700"
>
Active
</Badge>
</div>
<div className="space-y-3">
{/* Filter Keyword Section */}
<div className="rounded-lg bg-gray-50 p-3">
<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-900">
{chat.autoRule.query || "No keyword specified"}
</span>
</div>
</div>
<div className="rounded-lg bg-gray-50 p-3">
<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"
>
{type}
</Badge>
))
) : (
<span className="text-sm text-gray-500">
No file types selected
</span>
)}
</div>
</div>
</div>
</div>
)}
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4"> <div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4">
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700">
This will disable auto download for this chat. You can always This will disable auto download for this chat. You can always
enable it later. enable it later.
</p> </p>
</div> </div>
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-yellow-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700">
Files that are being downloaded will be paused and you can Files that are being downloaded will be paused and you can
enable automatic downloading again later. enable automatic downloading again later.
</p> </p>
</div> </div>
</div> </div>
</div>
) : ( ) : (
<div className="space-y-4">
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4"> <div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4">
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
@ -102,15 +181,26 @@ export default function AutoDownloadDialog() {
<div className="flex items-start"> <div className="flex items-start">
<span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span> <span className="mr-3 mt-1.5 h-3 w-2 flex-shrink-0 rounded-full bg-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700"> <p className="text-sm leading-6 text-gray-700">
Files in historical messages will be downloaded first, and then Files in historical messages will be downloaded first, and
files in new messages will be downloaded automatically. then files in new messages will be downloaded automatically.
</p> </p>
</div> </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">
Download Order:
<span className="ml-1 rounded bg-blue-100 px-2">
{"Photo -> Video -> Audio -> File"}
</span>
</p>
</div>
</div>
<AutoDownloadRule value={rule} onChange={setRule} />
</div> </div>
)} )}
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button
onClick={() => triggerAuto()} onClick={() => triggerAuto({ rule })}
variant={chat?.autoEnabled ? "destructive" : "default"} variant={chat?.autoEnabled ? "destructive" : "default"}
disabled={debounceIsAutoMutating} disabled={debounceIsAutoMutating}
> >
@ -128,3 +218,92 @@ export default function AutoDownloadDialog() {
</Dialog> </Dialog>
); );
} }
interface AutoDownloadRuleProps {
value: AutoDownloadRule;
onChange: (value: AutoDownloadRule) => void;
}
function AutoDownloadRule({ value, onChange }: AutoDownloadRuleProps) {
const handleQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
query: e.target.value,
});
};
const handleFileTypeSelect = (type: string) => {
if (value.fileTypes.includes(type as Exclude<FileType, "media">)) {
return;
}
onChange({
...value,
fileTypes: [...value.fileTypes, type as Exclude<FileType, "media">],
});
};
const removeFileType = (typeToRemove: string) => {
onChange({
...value,
fileTypes: value.fileTypes.filter((type) => type !== typeToRemove),
});
};
return (
<Accordion type="single" collapsible>
<AccordionItem value="advanced">
<AccordionTrigger className="hover:no-underline">
Advanced
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col space-y-4 rounded-md border p-4 shadow">
<div className="flex flex-col space-y-2">
<Label htmlFor="filter-keyword">Filter Keyword</Label>
<Input
id="filter-keyword"
type="text"
className="w-full"
placeholder="Enter a keyword to filter files"
value={value.query}
onChange={handleQueryChange}
/>
</div>
<div className="flex flex-col space-y-2">
<Label htmlFor="fileTypes">Filter File Types</Label>
<Select onValueChange={handleFileTypeSelect}>
<SelectTrigger id="fileTypes">
<SelectValue placeholder="Select File Types" />
</SelectTrigger>
<SelectContent>
{["photo", "video", "audio", "file"].map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="mt-2 flex flex-wrap gap-2">
{value.fileTypes.map((type) => (
<Badge
key={type}
className="flex items-center gap-1 px-2 py-1"
variant="secondary"
>
{type}
<X
className="h-3 w-3 cursor-pointer"
onClick={() => removeFileType(type)}
/>
</Badge>
))}
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
}

View file

@ -88,7 +88,7 @@ const axisStyle = {
}; };
const TelegramStats: React.FC<TelegramStatsProps> = ({ telegramId }) => { const TelegramStats: React.FC<TelegramStatsProps> = ({ telegramId }) => {
const [timeRange, setTimeRange] = useState<TimeRange>("2"); const [timeRange, setTimeRange] = useState<TimeRange>("1");
const { data, error, isLoading } = useSWR<ApiResponse, Error>( const { data, error, isLoading } = useSWR<ApiResponse, Error>(
`/telegram/${telegramId}/download-statistics?type=phase&timeRange=${timeRange}`, `/telegram/${telegramId}/download-statistics?type=phase&timeRange=${timeRange}`,

View file

@ -0,0 +1,57 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View file

@ -19,6 +19,7 @@ export type TelegramChat = {
lastMessage?: string; lastMessage?: string;
lastMessageTime?: string; lastMessageTime?: string;
autoEnabled: boolean; autoEnabled: boolean;
autoRule?: AutoDownloadRule;
}; };
export type FileType = "media" | "photo" | "video" | "audio" | "file"; export type FileType = "media" | "photo" | "video" | "audio" | "file";
@ -106,4 +107,9 @@ export type Proxy = {
password: string; password: string;
type: "http" | "socks5"; type: "http" | "socks5";
isEnabled?: boolean; isEnabled?: boolean;
} };
export type AutoDownloadRule = {
query: string;
fileTypes: Array<Exclude<FileType, "media">>;
};

View file

@ -33,15 +33,39 @@ export default {
}, },
}, },
breathing: { breathing: {
'0%, 100%': { transform: 'scale(1)', opacity: '0.7' }, "0%, 100%": {
'50%': { transform: 'scale(1.3)', opacity: '1' }, transform: "scale(1)",
opacity: "0.7",
},
"50%": {
transform: "scale(1.3)",
opacity: "1",
},
},
"accordion-down": {
from: {
height: "0",
},
to: {
height: "var(--radix-accordion-content-height)",
},
},
"accordion-up": {
from: {
height: "var(--radix-accordion-content-height)",
},
to: {
height: "0",
},
}, },
}, },
animation: { animation: {
"caret-blink": "caret-blink 1.25s ease-out infinite", "caret-blink": "caret-blink 1.25s ease-out infinite",
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear", "border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
orbit: "orbit calc(var(--duration)*1s) linear infinite", orbit: "orbit calc(var(--duration)*1s) linear infinite",
'breathing': 'breathing 6s ease-in-out infinite', breathing: "breathing 6s ease-in-out infinite",
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
}, },
fontFamily: { fontFamily: {
sans: ["var(--font-geist-sans)", ...fontFamily.sans], sans: ["var(--font-geist-sans)", ...fontFamily.sans],