feat: Support using proxies

This commit is contained in:
jarvis2f 2024-12-22 20:00:13 +08:00
parent 8086e8a485
commit d4ddc70639
17 changed files with 882 additions and 191 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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

View file

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

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

View file

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

View file

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