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.JsonObject;
import org.drinkless.tdlib.TdApi;
import org.jooq.lambda.tuple.Tuple2;
import telegram.files.repository.FileRecord;
import telegram.files.repository.SettingAutoRecords;
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 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
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));
return;
}
if (StrUtil.isBlank(auto.nextFileType)) {
auto.nextFileType = FILE_TYPE_ORDER.getFirst();
}
Tuple2<String, List<String>> rule = handleRule(auto);
TelegramVerticle telegramVerticle = this.getTelegramVerticle(auto.telegramId);
TdApi.SearchChatMessages searchChatMessages = new TdApi.SearchChatMessages();
searchChatMessages.query = rule.v1;
searchChatMessages.chatId = auto.chatId;
searchChatMessages.fromMessageId = auto.nextFromMessageId;
searchChatMessages.limit = Math.min(MAX_WAITING_LENGTH, 100);
@ -153,10 +154,11 @@ public class AutoDownloadVerticle extends AbstractVerticle {
return;
}
if (foundChatMessages.messages.length == 0) {
int nextTypeIndex = FILE_TYPE_ORDER.indexOf(auto.nextFileType) + 1;
if (nextTypeIndex < FILE_TYPE_ORDER.size()) {
List<String> fileTypes = rule.v2;
int nextTypeIndex = fileTypes.indexOf(auto.nextFileType) + 1;
if (nextTypeIndex < fileTypes.size()) {
String originalType = auto.nextFileType;
auto.nextFileType = FILE_TYPE_ORDER.get(nextTypeIndex);
auto.nextFileType = fileTypes.get(nextTypeIndex);
auto.nextFromMessageId = 0;
log.debug("%s No more %s files found! Switch to %s".formatted(auto.uniqueKey(), originalType, auto.nextFileType));
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) {
List<TdApi.Message> waitingMessages = this.waitingDownloadMessages.get(telegramId);
return getSurplusSize(telegramId) <= 0 || (waitingMessages != null && waitingMessages.size() > limit);

View file

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

View file

@ -3,6 +3,7 @@ package telegram.files;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DatePattern;
@ -439,7 +440,7 @@ public class TelegramVerticle extends AbstractVerticle {
.mapEmpty();
}
public Future<Void> toggleAutoDownload(Long chatId) {
public Future<Void> toggleAutoDownload(Long chatId, JsonObject params) {
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
.compose(settingAutoRecords -> {
if (settingAutoRecords == null) {
@ -448,7 +449,14 @@ public class TelegramVerticle extends AbstractVerticle {
if (settingAutoRecords.exists(this.telegramRecord.id(), chatId)) {
settingAutoRecords.remove(this.telegramRecord.id(), chatId);
} 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));
})
@ -824,19 +832,22 @@ public class TelegramVerticle extends AbstractVerticle {
private Future<JsonArray> convertChat(List<TdApi.Chat> chats) {
return DataVerticle.settingRepository.<SettingAutoRecords>getByKey(SettingKey.autoDownload)
.map(settingAutoRecords -> settingAutoRecords == null ? Collections.emptySet()
: settingAutoRecords.getChatIds(this.telegramRecord.id()))
.map(enableAutoChatIds -> new JsonArray(chats.stream()
.map(chat -> 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", enableAutoChatIds.contains(chat.id))
)
.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()
));
}

View file

@ -2,7 +2,8 @@ package telegram.files.repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class SettingAutoRecords {
@ -18,12 +19,15 @@ public class SettingAutoRecords {
public long nextFromMessageId;
public Rule rule;
public Item() {
}
public Item(long telegramId, long chatId) {
public Item(long telegramId, long chatId, Rule rule) {
this.telegramId = telegramId;
this.chatId = chatId;
this.rule = rule;
}
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() {
this.items = new ArrayList<>();
}
@ -48,19 +66,18 @@ public class SettingAutoRecords {
items.add(item);
}
public void add(long telegramId, long chatId) {
items.add(new Item(telegramId, chatId));
public void add(long telegramId, long chatId, Rule rule) {
items.add(new Item(telegramId, chatId, rule));
}
public void remove(long telegramId, long 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()
.filter(item -> item.telegramId == telegramId)
.map(item -> item.chatId)
.collect(Collectors.toSet());
.collect(Collectors.toMap(i -> i.chatId, Function.identity()));
}
}

333
web/package-lock.json generated
View file

@ -12,6 +12,7 @@
"@dnd-kit/modifiers": "^8.0.0",
"@dnd-kit/sortable": "^9.0.0",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
@ -970,6 +971,119 @@
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"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": {
"version": "1.1.0",
"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": {
"version": "1.1.0",
"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",
"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": {
"version": "1.1.0",
"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-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": {
"version": "1.1.0",
"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/sortable": "^9.0.0",
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",

View file

@ -7,7 +7,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import React, { useState } from "react";
import useSWRMutation from "swr/mutation";
import { POST } from "@/lib/api";
import { useDebounce } from "use-debounce";
@ -15,17 +15,41 @@ import { useToast } from "@/hooks/use-toast";
import { AutoDownloadButton } from "@/components/auto-download-button";
import { useTelegramChat } from "@/hooks/use-telegram-chat";
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() {
const { accountId } = useTelegramAccount();
const { isLoading, chat, reload } = useTelegramChat();
const { toast } = useToast();
const [open, setOpen] = useState(false);
const [rule, setRule] = useState<AutoDownloadRule>({
query: "",
fileTypes: [],
});
const { trigger: triggerAuto, isMutating: isAutoMutating } = useSWRMutation(
!accountId || !chat
? undefined
: `/file/auto-download?telegramId=${accountId}&chatId=${chat?.id}`,
POST,
(key, { arg }: { arg: { rule: AutoDownloadRule } }) => {
return POST(key, arg);
},
{
onSuccess: () => {
toast({
@ -74,43 +98,109 @@ export default function AutoDownloadDialog() {
</DialogHeader>
<DialogDescription></DialogDescription>
{chat?.autoEnabled ? (
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4">
<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">
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-cyan-400"></span>
<p className="text-sm leading-6 text-gray-700">
Files that are being downloaded will be paused and you can
enable automatic downloading again later.
</p>
<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="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">
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">
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 rounded-md border border-gray-200 bg-gray-50 p-4">
<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">
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">
Files in historical messages will be downloaded first, and then
files in new messages will be downloaded automatically.
</p>
<div className="space-y-4">
<div className="space-y-4 rounded-md border border-gray-200 bg-gray-50 p-4">
<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">
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">
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">
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 className="flex justify-end gap-2">
<Button
onClick={() => triggerAuto()}
onClick={() => triggerAuto({ rule })}
variant={chat?.autoEnabled ? "destructive" : "default"}
disabled={debounceIsAutoMutating}
>
@ -128,3 +218,92 @@ export default function AutoDownloadDialog() {
</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 [timeRange, setTimeRange] = useState<TimeRange>("2");
const [timeRange, setTimeRange] = useState<TimeRange>("1");
const { data, error, isLoading } = useSWR<ApiResponse, Error>(
`/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;
lastMessageTime?: string;
autoEnabled: boolean;
autoRule?: AutoDownloadRule;
};
export type FileType = "media" | "photo" | "video" | "audio" | "file";
@ -106,4 +107,9 @@ export type Proxy = {
password: string;
type: "http" | "socks5";
isEnabled?: boolean;
}
};
export type AutoDownloadRule = {
query: string;
fileTypes: Array<Exclude<FileType, "media">>;
};

View file

@ -33,15 +33,39 @@ export default {
},
},
breathing: {
'0%, 100%': { transform: 'scale(1)', opacity: '0.7' },
'50%': { transform: 'scale(1.3)', opacity: '1' },
"0%, 100%": {
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: {
"caret-blink": "caret-blink 1.25s ease-out infinite",
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
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: {
sans: ["var(--font-geist-sans)", ...fontFamily.sans],