✨ feat: Support using proxies
This commit is contained in:
parent
8086e8a485
commit
d4ddc70639
17 changed files with 882 additions and 191 deletions
|
|
@ -127,6 +127,8 @@ public class HttpVerticle extends AbstractVerticle {
|
||||||
router.get("/telegram/:telegramId/chat/:chatId/files/count").handler(this::handleTelegramFilesCount);
|
router.get("/telegram/:telegramId/chat/:chatId/files/count").handler(this::handleTelegramFilesCount);
|
||||||
router.get("/telegram/:telegramId/download-statistics").handler(this::handleTelegramDownloadStatistics);
|
router.get("/telegram/:telegramId/download-statistics").handler(this::handleTelegramDownloadStatistics);
|
||||||
router.post("/telegrams/change").handler(this::handleTelegramChange);
|
router.post("/telegrams/change").handler(this::handleTelegramChange);
|
||||||
|
router.post("/telegram/:telegramId/toggle-proxy").handler(this::handleTelegramToggleProxy);
|
||||||
|
router.get("/telegram/:telegramId/ping").handler(this::handleTelegramPing);
|
||||||
|
|
||||||
router.get("/file/preview").handler(this::handleFilePreview);
|
router.get("/file/preview").handler(this::handleFilePreview);
|
||||||
router.post("/file/start-download").handler(this::handleFileStartDownload);
|
router.post("/file/start-download").handler(this::handleFileStartDownload);
|
||||||
|
|
@ -273,9 +275,12 @@ public class HttpVerticle extends AbstractVerticle {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
JsonObject jsonObject = ctx.body().asJsonObject();
|
||||||
|
String proxyName = jsonObject.getString("proxyName");
|
||||||
|
|
||||||
TelegramVerticle newTelegramVerticle = new TelegramVerticle(DataVerticle.telegramRepository.getRootPath());
|
TelegramVerticle newTelegramVerticle = new TelegramVerticle(DataVerticle.telegramRepository.getRootPath());
|
||||||
newTelegramVerticle.bindHttpSession(sessionId);
|
newTelegramVerticle.bindHttpSession(sessionId);
|
||||||
|
newTelegramVerticle.setProxy(proxyName);
|
||||||
sessionTelegramVerticles.put(sessionId, newTelegramVerticle);
|
sessionTelegramVerticles.put(sessionId, newTelegramVerticle);
|
||||||
telegramVerticles.add(newTelegramVerticle);
|
telegramVerticles.add(newTelegramVerticle);
|
||||||
vertx.deployVerticle(newTelegramVerticle)
|
vertx.deployVerticle(newTelegramVerticle)
|
||||||
|
|
@ -401,6 +406,29 @@ public class HttpVerticle extends AbstractVerticle {
|
||||||
}, () -> ctx.fail(404));
|
}, () -> ctx.fail(404));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleTelegramToggleProxy(RoutingContext ctx) {
|
||||||
|
String telegramId = ctx.request().getParam("telegramId");
|
||||||
|
getTelegramVerticle(telegramId)
|
||||||
|
.ifPresentOrElse(telegramVerticle ->
|
||||||
|
telegramVerticle.toggleProxy(ctx.body().asJsonObject())
|
||||||
|
.onSuccess(r -> ctx.json(JsonObject.of("proxy", r)))
|
||||||
|
.onFailure(ctx::fail), () -> ctx.fail(404));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTelegramPing(RoutingContext ctx) {
|
||||||
|
String telegramId = ctx.pathParam("telegramId");
|
||||||
|
if (StrUtil.isBlank(telegramId)) {
|
||||||
|
ctx.fail(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getTelegramVerticle(telegramId)
|
||||||
|
.ifPresentOrElse(telegramVerticle ->
|
||||||
|
telegramVerticle.ping()
|
||||||
|
.onSuccess(r -> ctx.json(JsonObject.of("ping", r)))
|
||||||
|
.onFailure(ctx::fail), () -> ctx.fail(404)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private void handleTelegramApi(RoutingContext ctx) {
|
private void handleTelegramApi(RoutingContext ctx) {
|
||||||
String method = ctx.pathParam("method");
|
String method = ctx.pathParam("method");
|
||||||
if (method == null) {
|
if (method == null) {
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,7 @@ import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.core.util.TypeUtil;
|
import cn.hutool.core.util.TypeUtil;
|
||||||
import cn.hutool.log.Log;
|
import cn.hutool.log.Log;
|
||||||
import cn.hutool.log.LogFactory;
|
import cn.hutool.log.LogFactory;
|
||||||
import io.vertx.core.AbstractVerticle;
|
import io.vertx.core.*;
|
||||||
import io.vertx.core.CompositeFuture;
|
|
||||||
import io.vertx.core.Future;
|
|
||||||
import io.vertx.core.MultiMap;
|
|
||||||
import io.vertx.core.impl.NoStackTraceException;
|
import io.vertx.core.impl.NoStackTraceException;
|
||||||
import io.vertx.core.json.Json;
|
import io.vertx.core.json.Json;
|
||||||
import io.vertx.core.json.JsonArray;
|
import io.vertx.core.json.JsonArray;
|
||||||
|
|
@ -24,10 +21,7 @@ import org.drinkless.tdlib.Client;
|
||||||
import org.drinkless.tdlib.TdApi;
|
import org.drinkless.tdlib.TdApi;
|
||||||
import org.jooq.lambda.tuple.Tuple;
|
import org.jooq.lambda.tuple.Tuple;
|
||||||
import org.jooq.lambda.tuple.Tuple2;
|
import org.jooq.lambda.tuple.Tuple2;
|
||||||
import telegram.files.repository.FileRecord;
|
import telegram.files.repository.*;
|
||||||
import telegram.files.repository.SettingAutoRecords;
|
|
||||||
import telegram.files.repository.SettingKey;
|
|
||||||
import telegram.files.repository.TelegramRecord;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOError;
|
import java.io.IOError;
|
||||||
|
|
@ -50,6 +44,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
|
|
||||||
public String rootPath;
|
public String rootPath;
|
||||||
|
|
||||||
|
private String proxyName;
|
||||||
|
|
||||||
private String rootId;
|
private String rootId;
|
||||||
|
|
||||||
private boolean needDelete = false;
|
private boolean needDelete = false;
|
||||||
|
|
@ -74,6 +70,7 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
public TelegramVerticle(TelegramRecord telegramRecord) {
|
public TelegramVerticle(TelegramRecord telegramRecord) {
|
||||||
this.telegramRecord = telegramRecord;
|
this.telegramRecord = telegramRecord;
|
||||||
this.rootPath = telegramRecord.rootPath();
|
this.rootPath = telegramRecord.rootPath();
|
||||||
|
this.proxyName = telegramRecord.proxy();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRootId() {
|
public String getRootId() {
|
||||||
|
|
@ -87,14 +84,21 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
return telegramRecord == null ? this.getRootId() : telegramRecord.id();
|
return telegramRecord == null ? this.getRootId() : telegramRecord.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setProxy(String proxyName) {
|
||||||
|
this.proxyName = proxyName;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {
|
public void start(Promise<Void> startPromise) {
|
||||||
TelegramUpdateHandler telegramUpdateHandler = new TelegramUpdateHandler();
|
TelegramUpdateHandler telegramUpdateHandler = new TelegramUpdateHandler();
|
||||||
telegramUpdateHandler.setOnAuthorizationStateUpdated(this::onAuthorizationStateUpdated);
|
telegramUpdateHandler.setOnAuthorizationStateUpdated(this::onAuthorizationStateUpdated);
|
||||||
telegramUpdateHandler.setOnFileUpdated(this::onFileUpdated);
|
telegramUpdateHandler.setOnFileUpdated(this::onFileUpdated);
|
||||||
telegramUpdateHandler.setOnFileDownloadsUpdated(this::onFileDownloadsUpdated);
|
telegramUpdateHandler.setOnFileDownloadsUpdated(this::onFileDownloadsUpdated);
|
||||||
telegramUpdateHandler.setOnMessageReceived(this::onMessageReceived);
|
telegramUpdateHandler.setOnMessageReceived(this::onMessageReceived);
|
||||||
client = Client.create(telegramUpdateHandler, this::handleException, this::handleException);
|
client = Client.create(telegramUpdateHandler, this::handleException, this::handleException);
|
||||||
|
this.enableProxy(this.proxyName)
|
||||||
|
.onSuccess(r -> startPromise.complete())
|
||||||
|
.onFailure(startPromise::fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete() {
|
public void delete() {
|
||||||
|
|
@ -129,7 +133,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
.put("status", "inactive")
|
.put("status", "inactive")
|
||||||
.put("rootPath", this.rootPath)
|
.put("rootPath", this.rootPath)
|
||||||
.put("isPremium", false)
|
.put("isPremium", false)
|
||||||
.put("lastAuthorizationState", lastAuthorizationState);
|
.put("lastAuthorizationState", lastAuthorizationState)
|
||||||
|
.put("proxy", this.proxyName);
|
||||||
if (this.telegramRecord != null) {
|
if (this.telegramRecord != null) {
|
||||||
jsonObject.put("id", Convert.toStr(this.telegramRecord.id()))
|
jsonObject.put("id", Convert.toStr(this.telegramRecord.id()))
|
||||||
.put("name", this.telegramRecord.firstName());
|
.put("name", this.telegramRecord.firstName());
|
||||||
|
|
@ -146,7 +151,8 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
.put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(user, "profilePhoto.minithumbnail.data")))
|
.put("avatar", Base64.encode((byte[]) BeanUtil.getProperty(user, "profilePhoto.minithumbnail.data")))
|
||||||
.put("status", "active")
|
.put("status", "active")
|
||||||
.put("rootPath", this.rootPath)
|
.put("rootPath", this.rootPath)
|
||||||
.put("isPremium", user.isPremium);
|
.put("isPremium", user.isPremium)
|
||||||
|
.put("proxy", this.proxyName);
|
||||||
promise.complete(result);
|
promise.complete(result);
|
||||||
})
|
})
|
||||||
.onFailure(e -> {
|
.onFailure(e -> {
|
||||||
|
|
@ -158,6 +164,9 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
|
|
||||||
public Future<JsonArray> getChats(Long activatedChatId, String query) {
|
public Future<JsonArray> getChats(Long activatedChatId, String query) {
|
||||||
Consumer<TdApi.Chats> insertActivatedChatId = chats -> {
|
Consumer<TdApi.Chats> insertActivatedChatId = chats -> {
|
||||||
|
if (chats == null || ArrayUtil.isEmpty(chats.chatIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
long[] chatIds = chats.chatIds;
|
long[] chatIds = chats.chatIds;
|
||||||
if (activatedChatId != null && !ArrayUtil.contains(chatIds, activatedChatId)) {
|
if (activatedChatId != null && !ArrayUtil.contains(chatIds, activatedChatId)) {
|
||||||
chatIds = (long[]) ArrayUtil.insert(chatIds, 0, activatedChatId);
|
chatIds = (long[]) ArrayUtil.insert(chatIds, 0, activatedChatId);
|
||||||
|
|
@ -293,7 +302,9 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
if (fileRecord != null) {
|
if (fileRecord != null) {
|
||||||
promise.complete();
|
promise.complete();
|
||||||
} else {
|
} else {
|
||||||
DataVerticle.fileRepository.create(TdApiHelp.getFileHandler(message).get().convertFileRecord(telegramRecord.id()))
|
DataVerticle.fileRepository.create(TdApiHelp.getFileHandler(message)
|
||||||
|
.orElseThrow()
|
||||||
|
.convertFileRecord(telegramRecord.id()))
|
||||||
.onSuccess(r -> promise.complete())
|
.onSuccess(r -> promise.complete())
|
||||||
.onFailure(promise::fail);
|
.onFailure(promise::fail);
|
||||||
}
|
}
|
||||||
|
|
@ -402,6 +413,87 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
return DataVerticle.fileRepository.getDownloadStatistics(this.telegramRecord.id());
|
return DataVerticle.fileRepository.getDownloadStatistics(this.telegramRecord.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Future<TdApi.Proxy> enableProxy(String proxyName) {
|
||||||
|
if (StrUtil.isBlank(proxyName)) return Future.succeededFuture();
|
||||||
|
return DataVerticle.settingRepository.<SettingProxyRecords>getByKey(SettingKey.proxys)
|
||||||
|
.map(settingProxyRecords -> Optional.ofNullable(settingProxyRecords)
|
||||||
|
.flatMap(r -> r.getProxy(proxyName))
|
||||||
|
.orElseThrow(() -> new NoStackTraceException("Proxy %s not found".formatted(proxyName)))
|
||||||
|
)
|
||||||
|
.compose(proxy -> this.getTdProxy(proxy)
|
||||||
|
.map(r -> Tuple.tuple(proxy, r))
|
||||||
|
)
|
||||||
|
.compose(tuple -> {
|
||||||
|
SettingProxyRecords.Item proxy = tuple.v1;
|
||||||
|
TdApi.Proxy tdProxy = tuple.v2;
|
||||||
|
boolean edit = false;
|
||||||
|
if (tdProxy != null) {
|
||||||
|
if (tdProxy.isEnabled) {
|
||||||
|
return Future.succeededFuture(tdProxy);
|
||||||
|
}
|
||||||
|
edit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TdApi.ProxyType proxyType;
|
||||||
|
if (Objects.equals(proxy.type, "http")) {
|
||||||
|
proxyType = new TdApi.ProxyTypeHttp(proxy.username, proxy.password, false);
|
||||||
|
} else if (Objects.equals(proxy.type, "socks5")) {
|
||||||
|
proxyType = new TdApi.ProxyTypeSocks5(proxy.username, proxy.password);
|
||||||
|
} else {
|
||||||
|
return Future.failedFuture("Unsupported proxy type: %s".formatted(proxy.type));
|
||||||
|
}
|
||||||
|
return edit ? this.execute(new TdApi.EditProxy(tdProxy.id, proxy.server, proxy.port, true, proxyType))
|
||||||
|
: this.execute(new TdApi.AddProxy(proxy.server, proxy.port, true, proxyType));
|
||||||
|
})
|
||||||
|
.compose(r -> DataVerticle.telegramRepository.update(this.telegramRecord.withProxy(proxyName)).map(r))
|
||||||
|
.compose(r -> {
|
||||||
|
this.proxyName = proxyName;
|
||||||
|
return Future.succeededFuture(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<TdApi.Proxy> toggleProxy(JsonObject jsonObject) {
|
||||||
|
String toggleProxyName = jsonObject.getString("proxyName");
|
||||||
|
if (Objects.equals(toggleProxyName, this.proxyName)) {
|
||||||
|
return Future.succeededFuture();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StrUtil.isBlank(toggleProxyName) && StrUtil.isNotBlank(this.proxyName)) {
|
||||||
|
// disable proxy
|
||||||
|
return this.execute(new TdApi.DisableProxy())
|
||||||
|
.compose(r -> DataVerticle.telegramRepository.update(this.telegramRecord.withProxy(null)))
|
||||||
|
.andThen(r -> {
|
||||||
|
this.proxyName = null;
|
||||||
|
this.telegramRecord = r.result();
|
||||||
|
})
|
||||||
|
.mapEmpty();
|
||||||
|
} else {
|
||||||
|
return this.enableProxy(toggleProxyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<TdApi.Proxy> getTdProxy(SettingProxyRecords.Item proxy) {
|
||||||
|
return this.execute(new TdApi.GetProxies())
|
||||||
|
.map(proxies -> Stream.of(proxies.proxies)
|
||||||
|
.filter(proxy::equalsTdProxy)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<TdApi.Proxy> getTdProxy() {
|
||||||
|
return this.execute(new TdApi.GetProxies())
|
||||||
|
.map(proxies -> Stream.of(proxies.proxies)
|
||||||
|
.filter(p -> p.isEnabled)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Future<Double> ping() {
|
||||||
|
return this.getTdProxy()
|
||||||
|
.compose(proxy -> this.execute(new TdApi.PingProxy(proxy == null ? 0 : proxy.id)))
|
||||||
|
.map(r -> r.seconds);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <R extends TdApi.Object> Future<R> execute(TdApi.Function<R> method) {
|
public <R extends TdApi.Object> Future<R> execute(TdApi.Function<R> method) {
|
||||||
log.trace("[%s] Execute method: %s".formatted(getRootId(), TypeUtil.getTypeArgument(method.getClass())));
|
log.trace("[%s] Execute method: %s".formatted(getRootId(), TypeUtil.getTypeArgument(method.getClass())));
|
||||||
|
|
@ -506,7 +598,7 @@ public class TelegramVerticle extends AbstractVerticle {
|
||||||
if (telegramRecord == null) {
|
if (telegramRecord == null) {
|
||||||
this.execute(new TdApi.GetMe())
|
this.execute(new TdApi.GetMe())
|
||||||
.compose(user ->
|
.compose(user ->
|
||||||
DataVerticle.telegramRepository.create(new TelegramRecord(user.id, user.firstName, this.rootPath))
|
DataVerticle.telegramRepository.create(new TelegramRecord(user.id, user.firstName, this.rootPath, this.proxyName))
|
||||||
)
|
)
|
||||||
.compose(record -> {
|
.compose(record -> {
|
||||||
telegramRecord = record;
|
telegramRecord = record;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public enum SettingKey {
|
||||||
* Auto download limit for each telegram account
|
* Auto download limit for each telegram account
|
||||||
*/
|
*/
|
||||||
autoDownloadLimit(Convert::toInt),
|
autoDownloadLimit(Convert::toInt),
|
||||||
|
proxys(value -> new JsonObject(value).mapTo(SettingProxyRecords.class)),
|
||||||
;
|
;
|
||||||
|
|
||||||
public final Function<String, ?> converter;
|
public final Function<String, ?> converter;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package telegram.files.repository;
|
||||||
|
|
||||||
|
import org.drinkless.tdlib.TdApi;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class SettingProxyRecords {
|
||||||
|
|
||||||
|
public List<Item> items;
|
||||||
|
|
||||||
|
public static class Item {
|
||||||
|
/**
|
||||||
|
* should be unique
|
||||||
|
*/
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
public String server;
|
||||||
|
|
||||||
|
public int port;
|
||||||
|
|
||||||
|
public String username;
|
||||||
|
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* http, socks5
|
||||||
|
*/
|
||||||
|
public String type;
|
||||||
|
|
||||||
|
public boolean equalsTdProxy(TdApi.Proxy proxy) {
|
||||||
|
return Objects.equals(name, proxy.server)
|
||||||
|
&& port == proxy.port
|
||||||
|
&& (Objects.equals(type, "http") ? proxy.type instanceof TdApi.ProxyTypeHttp : proxy.type instanceof TdApi.ProxyTypeSocks5);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public SettingProxyRecords() {
|
||||||
|
this.items = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Item> getProxy(String name) {
|
||||||
|
return items.stream().filter(item -> Objects.equals(name, item.name)).findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,28 +4,38 @@ import cn.hutool.core.map.MapUtil;
|
||||||
import io.vertx.sqlclient.templates.RowMapper;
|
import io.vertx.sqlclient.templates.RowMapper;
|
||||||
import io.vertx.sqlclient.templates.TupleMapper;
|
import io.vertx.sqlclient.templates.TupleMapper;
|
||||||
|
|
||||||
import java.util.Map;
|
public record TelegramRecord(
|
||||||
|
long id,
|
||||||
public record TelegramRecord(long id, String firstName, String rootPath) {
|
String firstName,
|
||||||
|
String rootPath,
|
||||||
|
String proxy
|
||||||
|
) {
|
||||||
|
|
||||||
public static final String SCHEME = """
|
public static final String SCHEME = """
|
||||||
CREATE TABLE IF NOT EXISTS telegram_record
|
CREATE TABLE IF NOT EXISTS telegram_record
|
||||||
(
|
(
|
||||||
id BIGINT PRIMARY KEY,
|
id BIGINT PRIMARY KEY,
|
||||||
first_name VARCHAR(255),
|
first_name VARCHAR(255),
|
||||||
root_path VARCHAR(255)
|
root_path VARCHAR(255),
|
||||||
|
proxy VARCHAR(255)
|
||||||
)
|
)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
public static RowMapper<TelegramRecord> ROW_MAPPER = row ->
|
public static RowMapper<TelegramRecord> ROW_MAPPER = row ->
|
||||||
new TelegramRecord(row.getLong("id"),
|
new TelegramRecord(row.getLong("id"),
|
||||||
row.getString("first_name"),
|
row.getString("first_name"),
|
||||||
row.getString("root_path")
|
row.getString("root_path"),
|
||||||
|
row.getString("proxy")
|
||||||
);
|
);
|
||||||
|
|
||||||
public static TupleMapper<TelegramRecord> PARAM_MAPPER = TupleMapper.mapper(r ->
|
public static TupleMapper<TelegramRecord> PARAM_MAPPER = TupleMapper.mapper(r ->
|
||||||
Map.ofEntries(MapUtil.entry("id", r.id),
|
MapUtil.ofEntries(MapUtil.entry("id", r.id),
|
||||||
Map.entry("first_name", r.firstName()),
|
MapUtil.entry("first_name", r.firstName()),
|
||||||
Map.entry("root_path", r.rootPath())
|
MapUtil.entry("root_path", r.rootPath()),
|
||||||
|
MapUtil.entry("proxy", r.proxy())
|
||||||
));
|
));
|
||||||
|
|
||||||
|
public TelegramRecord withProxy(String proxy) {
|
||||||
|
return new TelegramRecord(id, firstName, rootPath, proxy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ public interface TelegramRepository {
|
||||||
|
|
||||||
Future<TelegramRecord> create(TelegramRecord telegramRecord);
|
Future<TelegramRecord> create(TelegramRecord telegramRecord);
|
||||||
|
|
||||||
|
Future<TelegramRecord> update(TelegramRecord telegramRecord);
|
||||||
|
|
||||||
Future<TelegramRecord> getById(long id);
|
Future<TelegramRecord> getById(long id);
|
||||||
|
|
||||||
Future<List<TelegramRecord>> getAll();
|
Future<List<TelegramRecord>> getAll();
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public class TelegramRepositoryImpl implements TelegramRepository {
|
||||||
@Override
|
@Override
|
||||||
public Future<TelegramRecord> create(TelegramRecord telegramRecord) {
|
public Future<TelegramRecord> create(TelegramRecord telegramRecord) {
|
||||||
return SqlTemplate
|
return SqlTemplate
|
||||||
.forUpdate(pool, "INSERT INTO telegram_record(id, first_name, root_path) VALUES (#{id}, #{first_name}, #{root_path})")
|
.forUpdate(pool, "INSERT INTO telegram_record(id, first_name, root_path, proxy) VALUES (#{id}, #{first_name}, #{root_path}, #{proxy})")
|
||||||
.mapFrom(TelegramRecord.PARAM_MAPPER)
|
.mapFrom(TelegramRecord.PARAM_MAPPER)
|
||||||
.execute(telegramRecord)
|
.execute(telegramRecord)
|
||||||
.map(r -> telegramRecord)
|
.map(r -> telegramRecord)
|
||||||
|
|
@ -57,6 +57,19 @@ public class TelegramRepositoryImpl implements TelegramRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<TelegramRecord> update(TelegramRecord telegramRecord) {
|
||||||
|
return SqlTemplate
|
||||||
|
.forUpdate(pool, "UPDATE telegram_record SET first_name = #{first_name}, root_path = #{root_path}, proxy = #{proxy} WHERE id = #{id}")
|
||||||
|
.mapFrom(TelegramRecord.PARAM_MAPPER)
|
||||||
|
.execute(telegramRecord)
|
||||||
|
.map(r -> telegramRecord)
|
||||||
|
.onSuccess(r -> log.debug("Successfully updated telegram record: %s".formatted(telegramRecord.id())))
|
||||||
|
.onFailure(
|
||||||
|
err -> log.error("Failed to update telegram record: %s".formatted(err.getMessage()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<TelegramRecord> getById(long id) {
|
public Future<TelegramRecord> getById(long id) {
|
||||||
return SqlTemplate
|
return SqlTemplate
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ Cookie: {{tf}}
|
||||||
POST http://{{server}}/telegram/create
|
POST http://{{server}}/telegram/create
|
||||||
Cookie: {{tf}}
|
Cookie: {{tf}}
|
||||||
|
|
||||||
|
### Ping
|
||||||
|
GET http://{{server}}/telegram/{{telegramId}}/ping
|
||||||
|
Cookie: {{tf}}
|
||||||
|
|
||||||
### Get chats
|
### Get chats
|
||||||
GET http://{{server}}/telegram/{{telegramId}}/chats
|
GET http://{{server}}/telegram/{{telegramId}}/chats
|
||||||
Cookie: {{tf}}
|
Cookie: {{tf}}
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,14 @@ import {
|
||||||
} from "@/lib/websocket-types";
|
} from "@/lib/websocket-types";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import useSWRMutation from "swr/mutation";
|
import useSWRMutation from "swr/mutation";
|
||||||
import { POST, telegramApi, type TelegramApiArg } from "@/lib/api";
|
import { request, telegramApi, type TelegramApiArg } from "@/lib/api";
|
||||||
import { useTelegramAccount } from "@/hooks/use-telegram-account";
|
import { useTelegramAccount } from "@/hooks/use-telegram-account";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { type TelegramApiResult } from "@/lib/types";
|
import { type TelegramApiResult } from "@/lib/types";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
|
||||||
|
import ProxysDialog from "@/components/proxys-dialog";
|
||||||
|
|
||||||
export function AccountDialog({
|
export function AccountDialog({
|
||||||
children,
|
children,
|
||||||
|
|
@ -37,12 +38,24 @@ export function AccountDialog({
|
||||||
}) {
|
}) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
|
const [proxyName, setProxyName] = useState<string | undefined>();
|
||||||
const {
|
const {
|
||||||
data: createData,
|
data: createData,
|
||||||
trigger: triggerCreate,
|
trigger: triggerCreate,
|
||||||
isMutating: isCreateMutating,
|
isMutating: isCreateMutating,
|
||||||
error: createError,
|
error: createError,
|
||||||
} = useSWRMutation<{ id: string }, Error>("/telegram/create", POST);
|
} = useSWRMutation<{ id: string }, Error>(
|
||||||
|
"/telegram/create",
|
||||||
|
async (key: string) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return await request(key, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
proxyName: proxyName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
const { trigger: triggerMethod, isMutating: isMethodMutating } =
|
const { trigger: triggerMethod, isMutating: isMethodMutating } =
|
||||||
useSWRMutation<TelegramApiResult, Error, string, TelegramApiArg>(
|
useSWRMutation<TelegramApiResult, Error, string, TelegramApiArg>(
|
||||||
"/telegram/api",
|
"/telegram/api",
|
||||||
|
|
@ -137,7 +150,6 @@ export function AccountDialog({
|
||||||
if (isMethodMutating) return true;
|
if (isMethodMutating) return true;
|
||||||
const lastCode = methodCodes[methodCodes.length - 1];
|
const lastCode = methodCodes[methodCodes.length - 1];
|
||||||
if (!lastCode) return false;
|
if (!lastCode) return false;
|
||||||
console.log(lastCode, methodCompleteCodes);
|
|
||||||
return !methodCompleteCodes.includes(lastCode);
|
return !methodCompleteCodes.includes(lastCode);
|
||||||
}, [isMethodMutating, methodCodes, methodCompleteCodes]);
|
}, [isMethodMutating, methodCodes, methodCompleteCodes]);
|
||||||
|
|
||||||
|
|
@ -173,7 +185,7 @@ export function AccountDialog({
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
aria-describedby={undefined}
|
aria-describedby={undefined}
|
||||||
className="h-full w-full md:h-auto md:min-h-40 md:min-w-[550px]"
|
className="relative h-full w-full pb-10 md:h-auto md:min-h-40 md:min-w-[550px]"
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
|
@ -184,7 +196,7 @@ export function AccountDialog({
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{!debounceIsCreateMutating && !initSuccessfully && (
|
{!debounceIsCreateMutating && !initSuccessfully && (
|
||||||
<div className="flex items-center justify-center space-x-2">
|
<div className="flex flex-col items-center justify-center space-y-4">
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full",
|
"w-full",
|
||||||
|
|
@ -291,6 +303,13 @@ export function AccountDialog({
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
<ProxysDialog
|
||||||
|
telegramId={createData?.id}
|
||||||
|
proxyName={proxyName}
|
||||||
|
onProxyNameChange={setProxyName}
|
||||||
|
enableSelect={true}
|
||||||
|
className="absolute bottom-1 right-1"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { AccountDialog } from "@/components/account-dialog";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { BorderBeam } from "@/components/ui/border-beam";
|
import { BorderBeam } from "@/components/ui/border-beam";
|
||||||
|
import ProxysDialog from "@/components/proxys-dialog";
|
||||||
|
|
||||||
interface EmptyStateProps {
|
interface EmptyStateProps {
|
||||||
hasAccounts: boolean;
|
hasAccounts: boolean;
|
||||||
|
|
@ -55,15 +56,19 @@ export function EmptyState({
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<AccountDialog isAdd={true}>
|
<div className="flex items-center justify-center space-x-4">
|
||||||
<div className="relative rounded-md">
|
<AccountDialog isAdd={true}>
|
||||||
<BorderBeam size={60} duration={12} delay={9} />
|
<div className="relative rounded-md">
|
||||||
<Button variant="outline">
|
<BorderBeam size={60} duration={12} delay={9} />
|
||||||
<UserPlus className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Add Account
|
<UserPlus className="mr-2 h-4 w-4" />
|
||||||
</Button>
|
Add Account
|
||||||
</div>
|
</Button>
|
||||||
</AccountDialog>
|
</div>
|
||||||
|
</AccountDialog>
|
||||||
|
|
||||||
|
<ProxysDialog />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasAccounts && accounts.length > 0 && onSelectAccount && (
|
{hasAccounts && accounts.length > 0 && onSelectAccount && (
|
||||||
|
|
|
||||||
49
web/src/components/proxy-ping.tsx
Normal file
49
web/src/components/proxy-ping.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { ChevronsLeftRightEllipsis } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
export default function ProxyPing({ accountId }: { accountId: string }) {
|
||||||
|
const { data, isLoading, error, mutate } = useSWR<
|
||||||
|
{
|
||||||
|
ping: number;
|
||||||
|
},
|
||||||
|
Error
|
||||||
|
>(`/telegram/${accountId}/ping`, {
|
||||||
|
errorRetryCount: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div
|
||||||
|
className="flex items-center space-x-2 rounded bg-gray-100 p-1"
|
||||||
|
onClick={() => {
|
||||||
|
if (isLoading) return;
|
||||||
|
void mutate(undefined, true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronsLeftRightEllipsis className="h-4 w-4 text-gray-500" />
|
||||||
|
{isLoading && (
|
||||||
|
<div className="h-6 w-24 animate-pulse rounded bg-gray-200"></div>
|
||||||
|
)}
|
||||||
|
{!isLoading && error && <span>Connection error</span>}
|
||||||
|
{!isLoading && data && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>Ping: {(data.ping * 1000).toFixed(0)}ms</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Current latency for accessing telegram API
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
web/src/components/proxys-dialog.tsx
Normal file
48
web/src/components/proxys-dialog.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { EarthLock } from "lucide-react";
|
||||||
|
import Proxys, { type ProxysProps } from "@/components/proxys";
|
||||||
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
|
import React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type ProxysDialogProps = {
|
||||||
|
className?: string;
|
||||||
|
} & ProxysProps;
|
||||||
|
|
||||||
|
export default function ProxysDialog({
|
||||||
|
className,
|
||||||
|
...proxyProps
|
||||||
|
}: ProxysDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className={cn(
|
||||||
|
"h-auto rounded-full bg-primary px-2 text-accent hover:bg-primary/90 hover:text-accent",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<EarthLock className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className="md:max-w-2/3 h-full w-full max-w-full md:h-3/4 md:w-2/3"
|
||||||
|
aria-describedby={undefined}
|
||||||
|
>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<DialogTitle>Proxys</DialogTitle>
|
||||||
|
</VisuallyHidden>
|
||||||
|
<div className="mt-3">
|
||||||
|
<Proxys {...proxyProps} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
337
web/src/components/proxys.tsx
Normal file
337
web/src/components/proxys.tsx
Normal file
|
|
@ -0,0 +1,337 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useSettings } from "@/hooks/use-settings";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Edit2, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { type Proxy } from "@/lib/types";
|
||||||
|
import { BorderBeam } from "@/components/ui/border-beam";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import useSWRMutation from "swr/mutation";
|
||||||
|
import { request } from "@/lib/api";
|
||||||
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
import ProxyPing from "@/components/proxy-ping";
|
||||||
|
|
||||||
|
export interface ProxysProps {
|
||||||
|
enableSelect?: boolean;
|
||||||
|
telegramId?: string;
|
||||||
|
proxyName?: string;
|
||||||
|
onProxyNameChange?: (name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Proxys({
|
||||||
|
enableSelect,
|
||||||
|
telegramId,
|
||||||
|
proxyName,
|
||||||
|
onProxyNameChange,
|
||||||
|
}: ProxysProps) {
|
||||||
|
const { settings, updateSettings } = useSettings();
|
||||||
|
const [innerProxyName, setInnerProxyName] = useState("");
|
||||||
|
const [isDialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [editingProxy, setEditingProxy] = useState<Proxy | null>(null);
|
||||||
|
const [formState, setFormState] = useState<Proxy>({
|
||||||
|
name: "",
|
||||||
|
server: "",
|
||||||
|
port: 0,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
type: "http",
|
||||||
|
});
|
||||||
|
const { trigger: triggerProxy, isMutating: isToggleProxyMutating } =
|
||||||
|
useSWRMutation<{ id: string }, Error>(
|
||||||
|
telegramId ? `/telegram/${telegramId}/toggle-proxy` : undefined,
|
||||||
|
async (key: string) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return await request(key, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
proxyName: innerProxyName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: innerProxyName
|
||||||
|
? `Proxy is set to ${innerProxyName}`
|
||||||
|
: "Proxy is disabled",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse proxy settings
|
||||||
|
const proxys = (
|
||||||
|
(settings?.proxys
|
||||||
|
? JSON.parse(settings.proxys)
|
||||||
|
: {
|
||||||
|
items: [],
|
||||||
|
}) as {
|
||||||
|
items: Proxy[];
|
||||||
|
}
|
||||||
|
).items;
|
||||||
|
|
||||||
|
// Open dialog for adding or editing proxy
|
||||||
|
const handleOpenDialog = (proxy: Proxy | null = null): void => {
|
||||||
|
setEditingProxy(proxy);
|
||||||
|
setFormState(
|
||||||
|
proxy ?? {
|
||||||
|
name: "",
|
||||||
|
server: "",
|
||||||
|
port: 0,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
type: "http",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close dialog
|
||||||
|
const handleCloseDialog = (): void => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setEditingProxy(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete proxy with confirmation
|
||||||
|
const handleDeleteProxy = (proxy: Proxy): void => {
|
||||||
|
if (confirm(`Are you sure you want to delete the proxy ${proxy.name}?`)) {
|
||||||
|
const updatedProxys = proxys.filter((p) => p.name !== proxy.name);
|
||||||
|
void updateSettings({
|
||||||
|
proxys: JSON.stringify({
|
||||||
|
items: updatedProxys,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save proxy (either add or edit)
|
||||||
|
const handleSaveProxy = (): void => {
|
||||||
|
const updatedProxys = editingProxy
|
||||||
|
? proxys.map((p) => (p.name === editingProxy.name ? formState : p))
|
||||||
|
: [...proxys, formState];
|
||||||
|
void updateSettings({ proxys: JSON.stringify({ items: updatedProxys }) }); // Submit to server
|
||||||
|
handleCloseDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle form input change
|
||||||
|
const handleInputChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||||
|
): void => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: name === "port" ? parseInt(value, 10) : value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProxySubmit = async () => {
|
||||||
|
if (telegramId) {
|
||||||
|
await triggerProxy();
|
||||||
|
} else {
|
||||||
|
onProxyNameChange?.(innerProxyName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<h1 className="text-2xl font-bold">Proxys</h1>
|
||||||
|
{telegramId && <ProxyPing accountId={telegramId} />}
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => handleOpenDialog()}>
|
||||||
|
<Plus className="mr-2 h-5 w-5" /> Add Proxy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{proxys.map((proxy) => (
|
||||||
|
<Card
|
||||||
|
key={proxy.name}
|
||||||
|
className={cn("relative hover:shadow-lg", {
|
||||||
|
"cursor-pointer": enableSelect,
|
||||||
|
"border border-primary":
|
||||||
|
enableSelect && proxy.name === innerProxyName,
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
if (innerProxyName === proxy.name) {
|
||||||
|
setInnerProxyName("");
|
||||||
|
} else {
|
||||||
|
setInnerProxyName(proxy.name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{proxyName && proxy.name === proxyName && (
|
||||||
|
<BorderBeam size={100} duration={12} delay={9} />
|
||||||
|
)}
|
||||||
|
<CardHeader className="p-2 px-3">
|
||||||
|
<CardTitle className="text-xl font-semibold">
|
||||||
|
{proxy.name}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0 px-3">
|
||||||
|
<div className="flex items-baseline space-x-2">
|
||||||
|
<p className="text-lg text-gray-700">
|
||||||
|
{proxy.type.toUpperCase()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-400">{`${proxy.server}:${proxy.port}`}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-end space-x-2 p-1">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleOpenDialog(proxy)}
|
||||||
|
>
|
||||||
|
<Edit2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleDeleteProxy(proxy)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3 text-red-600" />
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{enableSelect && (
|
||||||
|
<Button
|
||||||
|
className="absolute bottom-4 right-4"
|
||||||
|
disabled={isToggleProxyMutating}
|
||||||
|
onClick={() => handleProxySubmit()}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={handleCloseDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingProxy ? "Edit Proxy" : "Add Proxy"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Radio buttons for proxy type */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-lg text-gray-700">Type</label>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="type"
|
||||||
|
value="http"
|
||||||
|
checked={formState.type === "http"}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<span>HTTP</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="type"
|
||||||
|
value="socks5"
|
||||||
|
checked={formState.type === "socks5"}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<span>SOCKS5</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Other input fields */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-lg text-gray-700">Name</label>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
value={formState.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter proxy name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="mb-1 text-lg text-gray-700">
|
||||||
|
Proxy server address and port number
|
||||||
|
</p>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-sm font-medium text-gray-500">
|
||||||
|
Server
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
name="server"
|
||||||
|
value={formState.server}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter server address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-24">
|
||||||
|
<label className="block text-sm font-medium text-gray-500">
|
||||||
|
Port
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
name="port"
|
||||||
|
type="number"
|
||||||
|
value={formState.port}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter port number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mb-1 text-lg text-gray-700">
|
||||||
|
Authentication (optional)
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
name="username"
|
||||||
|
value={formState.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-500">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={formState.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={handleCloseDialog} variant="ghost">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSaveProxy}>
|
||||||
|
{editingProxy ? "Save" : "Add"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { type FormEvent, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -6,45 +6,17 @@ import {
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Settings } from "lucide-react";
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Bell, Settings } from "lucide-react";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { useSettings } from "@/hooks/use-settings";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import FileStatistics from "@/components/file-statistics";
|
import FileStatistics from "@/components/file-statistics";
|
||||||
import { useTelegramAccount } from "@/hooks/use-telegram-account";
|
import { useTelegramAccount } from "@/hooks/use-telegram-account";
|
||||||
|
import Proxys from "@/components/proxys";
|
||||||
|
import SettingsForm from "@/components/settings-form";
|
||||||
|
|
||||||
export const SettingsDialog: React.FC = () => {
|
export const SettingsDialog: React.FC = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { accountId } = useTelegramAccount();
|
const { account, accountId } = useTelegramAccount();
|
||||||
|
|
||||||
const { settings, setSetting, updateSettings } = useSettings();
|
|
||||||
|
|
||||||
const imageLoadSizeOptions = [
|
|
||||||
{ value: "s", label: "box 100x100" },
|
|
||||||
{ value: "m", label: "box 320x320" },
|
|
||||||
{ value: "x", label: "box 800x800" },
|
|
||||||
{ value: "y", label: "box 1280x1280" },
|
|
||||||
{ value: "w", label: "box 2560x2560" },
|
|
||||||
{ value: "a", label: "crop 160x160" },
|
|
||||||
{ value: "b", label: "crop 320x320" },
|
|
||||||
{ value: "c", label: "crop 640x640" },
|
|
||||||
{ value: "d", label: "crop 1280x1280" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleSave = async (e: FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await updateSettings();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
|
@ -73,128 +45,34 @@ export const SettingsDialog: React.FC = () => {
|
||||||
<TabsList className="justify-start">
|
<TabsList className="justify-start">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="statistics">Statistics</TabsTrigger>
|
<TabsTrigger value="statistics">Statistics</TabsTrigger>
|
||||||
|
<TabsTrigger value="proxys">Proxys</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent
|
<TabsContent value="general" className="h-full">
|
||||||
value="general"
|
<div className="flex flex-col overflow-hidden">
|
||||||
className="flex flex-col overflow-hidden"
|
<SettingsForm />
|
||||||
>
|
</div>
|
||||||
<form onSubmit={handleSave} className="flex flex-col space-y-4 overflow-y-scroll">
|
|
||||||
<div className="flex flex-col space-y-4 overflow-y-scroll">
|
|
||||||
<p className="rounded-md bg-gray-50 p-2 text-sm text-muted-foreground shadow">
|
|
||||||
<Bell className="mr-2 inline-block h-4 w-4" />
|
|
||||||
These settings will be applied to all accounts.
|
|
||||||
</p>
|
|
||||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Label htmlFor="unique-only">Unique Only</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="unique-only"
|
|
||||||
checked={settings?.uniqueOnly === "true"}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
void setSetting("uniqueOnly", String(checked))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Show only unique file in the table. If disabled, will show
|
|
||||||
all. <br />
|
|
||||||
<strong>Warning:</strong> If enabled, the number of
|
|
||||||
documents on the form will be inaccurate.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Label htmlFor="need-load-preview-images">
|
|
||||||
Need Load Preview Images
|
|
||||||
</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="need-load-preview-images"
|
|
||||||
checked={settings?.needToLoadImages === "true"}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
void setSetting("needToLoadImages", String(checked));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{settings?.needToLoadImages === "true" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="image-load-size">Load Size</Label>
|
|
||||||
<Select
|
|
||||||
value={settings.imageLoadSize}
|
|
||||||
onValueChange={(v) =>
|
|
||||||
void setSetting("imageLoadSize", v)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="image-load-size">
|
|
||||||
<SelectValue placeholder="Select Image Load Size" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{imageLoadSizeOptions.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
The size of the image to load in the browser.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Label htmlFor="show-sensitive-content">
|
|
||||||
Show Sensitive Content
|
|
||||||
</Label>
|
|
||||||
<Checkbox
|
|
||||||
id="show-sensitive-content"
|
|
||||||
checked={settings?.showSensitiveContent === "true"}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
void setSetting("showSensitiveContent", String(checked))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Show sensitive content in the table, Will use a spoiler to
|
|
||||||
hide sensitive content if disabled.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
|
||||||
<Label>Auto Download Settings</Label>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<Label htmlFor="limit">Limit Per Account</Label>
|
|
||||||
<Input
|
|
||||||
id="limit"
|
|
||||||
className="w-24"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
value={settings?.autoDownloadLimit ?? 5}
|
|
||||||
onChange={(e) => {
|
|
||||||
void setSetting("autoDownloadLimit", e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex flex-1 justify-end">
|
|
||||||
<Button type="submit">Submit</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent
|
<TabsContent value="statistics" className="h-full">
|
||||||
value="statistics"
|
<div className="flex flex-col overflow-y-scroll">
|
||||||
className="flex flex-col overflow-y-scroll"
|
{accountId ? (
|
||||||
>
|
<FileStatistics telegramId={accountId} />
|
||||||
{accountId ? (
|
) : (
|
||||||
<FileStatistics telegramId={accountId} />
|
<div className="flex flex-1 items-center justify-center">
|
||||||
) : (
|
<p className="text-lg text-muted-foreground">
|
||||||
<div className="flex flex-1 items-center justify-center">
|
Please select an account to view statistics
|
||||||
<p className="text-lg text-muted-foreground">
|
</p>
|
||||||
Please select an account to view statistics
|
</div>
|
||||||
</p>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</TabsContent>
|
||||||
|
<TabsContent value="proxys" className="h-full">
|
||||||
|
<div className="flex h-full flex-col overflow-y-scroll">
|
||||||
|
<Proxys
|
||||||
|
telegramId={accountId}
|
||||||
|
proxyName={account?.proxy}
|
||||||
|
enableSelect={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
142
web/src/components/settings-form.tsx
Normal file
142
web/src/components/settings-form.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { Bell } from "lucide-react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import React, { type FormEvent } from "react";
|
||||||
|
import { useSettings } from "@/hooks/use-settings";
|
||||||
|
|
||||||
|
export default function SettingsForm() {
|
||||||
|
const { settings, setSetting, updateSettings } = useSettings();
|
||||||
|
|
||||||
|
const imageLoadSizeOptions = [
|
||||||
|
{ value: "s", label: "box 100x100" },
|
||||||
|
{ value: "m", label: "box 320x320" },
|
||||||
|
{ value: "x", label: "box 800x800" },
|
||||||
|
{ value: "y", label: "box 1280x1280" },
|
||||||
|
{ value: "w", label: "box 2560x2560" },
|
||||||
|
{ value: "a", label: "crop 160x160" },
|
||||||
|
{ value: "b", label: "crop 320x320" },
|
||||||
|
{ value: "c", label: "crop 640x640" },
|
||||||
|
{ value: "d", label: "crop 1280x1280" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSave = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await updateSettings();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSave}
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 flex-col space-y-4 overflow-y-scroll">
|
||||||
|
<p className="rounded-md bg-gray-50 p-2 text-sm text-muted-foreground shadow">
|
||||||
|
<Bell className="mr-2 inline-block h-4 w-4" />
|
||||||
|
These settings will be applied to all accounts.
|
||||||
|
</p>
|
||||||
|
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Label htmlFor="unique-only">Unique Only</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="unique-only"
|
||||||
|
checked={settings?.uniqueOnly === "true"}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
void setSetting("uniqueOnly", String(checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Show only unique file in the table. If disabled, will show all.{" "}
|
||||||
|
<br />
|
||||||
|
<strong>Warning:</strong> If enabled, the number of documents on the
|
||||||
|
form will be inaccurate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Label htmlFor="need-load-preview-images">
|
||||||
|
Need Load Preview Images
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="need-load-preview-images"
|
||||||
|
checked={settings?.needToLoadImages === "true"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
void setSetting("needToLoadImages", String(checked));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{settings?.needToLoadImages === "true" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="image-load-size">Load Size</Label>
|
||||||
|
<Select
|
||||||
|
value={settings.imageLoadSize}
|
||||||
|
onValueChange={(v) => void setSetting("imageLoadSize", v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="image-load-size">
|
||||||
|
<SelectValue placeholder="Select Image Load Size" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{imageLoadSizeOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
The size of the image to load in the browser.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Label htmlFor="show-sensitive-content">
|
||||||
|
Show Sensitive Content
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
id="show-sensitive-content"
|
||||||
|
checked={settings?.showSensitiveContent === "true"}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
void setSetting("showSensitiveContent", String(checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Show sensitive content in the table, Will use a spoiler to hide
|
||||||
|
sensitive content if disabled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col space-y-4 rounded-md border p-4 shadow">
|
||||||
|
<Label>Auto Download Settings</Label>
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<Label htmlFor="limit">Limit Per Account</Label>
|
||||||
|
<Input
|
||||||
|
id="limit"
|
||||||
|
className="w-24"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
value={settings?.autoDownloadLimit ?? 5}
|
||||||
|
onChange={(e) => {
|
||||||
|
void setSetting("autoDownloadLimit", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ interface SettingsContextType {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
settings?: Settings;
|
settings?: Settings;
|
||||||
setSetting: (key: SettingKey, value: string) => Promise<void>;
|
setSetting: (key: SettingKey, value: string) => Promise<void>;
|
||||||
updateSettings: (updates?: Settings) => Promise<void>;
|
updateSettings: (updates?: Partial<Settings>) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsContext = createContext<SettingsContextType | undefined>(
|
const SettingsContext = createContext<SettingsContextType | undefined>(
|
||||||
|
|
@ -41,13 +41,15 @@ export const SettingsProvider: React.FC<SettingsProviderProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新配置项方法
|
// 更新配置项方法
|
||||||
const updateSettings = async (updates?: Settings) => {
|
const updateSettings = async (updates?: Partial<Settings>) => {
|
||||||
await request("/settings/create", {
|
await request("/settings/create", {
|
||||||
body: updates ? JSON.stringify(updates) : JSON.stringify(settings),
|
body: updates ? JSON.stringify(updates) : JSON.stringify(settings),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
// 更新缓存
|
// 更新缓存
|
||||||
if (updates) {
|
if (updates) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
await mutate((current) => ({ ...current, ...updates }), false);
|
await mutate((current) => ({ ...current, ...updates }), false);
|
||||||
}
|
}
|
||||||
toast({ title: "Success", description: "Settings updated" });
|
toast({ title: "Success", description: "Settings updated" });
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export type TelegramAccount = {
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
status: "active" | "inactive";
|
status: "active" | "inactive";
|
||||||
lastAuthorizationState?: TelegramObject;
|
lastAuthorizationState?: TelegramObject;
|
||||||
|
proxy?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TelegramChat = {
|
export type TelegramChat = {
|
||||||
|
|
@ -85,8 +86,20 @@ export const SettingKeys = [
|
||||||
"imageLoadSize",
|
"imageLoadSize",
|
||||||
"showSensitiveContent",
|
"showSensitiveContent",
|
||||||
"autoDownloadLimit",
|
"autoDownloadLimit",
|
||||||
|
"proxys",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SettingKey = (typeof SettingKeys)[number];
|
export type SettingKey = (typeof SettingKeys)[number];
|
||||||
|
|
||||||
export type Settings = Record<SettingKey, string>;
|
export type Settings = Record<SettingKey, string>;
|
||||||
|
|
||||||
|
export type Proxy = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
server: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
type: "http" | "socks5";
|
||||||
|
isEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue