diff --git a/api/src/main/java/telegram/files/HttpVerticle.java b/api/src/main/java/telegram/files/HttpVerticle.java index 2571534..7ac62d3 100644 --- a/api/src/main/java/telegram/files/HttpVerticle.java +++ b/api/src/main/java/telegram/files/HttpVerticle.java @@ -127,6 +127,8 @@ public class HttpVerticle extends AbstractVerticle { router.get("/telegram/:telegramId/chat/:chatId/files/count").handler(this::handleTelegramFilesCount); router.get("/telegram/:telegramId/download-statistics").handler(this::handleTelegramDownloadStatistics); 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.post("/file/start-download").handler(this::handleFileStartDownload); @@ -273,9 +275,12 @@ public class HttpVerticle extends AbstractVerticle { ); return; } + JsonObject jsonObject = ctx.body().asJsonObject(); + String proxyName = jsonObject.getString("proxyName"); TelegramVerticle newTelegramVerticle = new TelegramVerticle(DataVerticle.telegramRepository.getRootPath()); newTelegramVerticle.bindHttpSession(sessionId); + newTelegramVerticle.setProxy(proxyName); sessionTelegramVerticles.put(sessionId, newTelegramVerticle); telegramVerticles.add(newTelegramVerticle); vertx.deployVerticle(newTelegramVerticle) @@ -401,6 +406,29 @@ public class HttpVerticle extends AbstractVerticle { }, () -> 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) { String method = ctx.pathParam("method"); if (method == null) { diff --git a/api/src/main/java/telegram/files/TelegramVerticle.java b/api/src/main/java/telegram/files/TelegramVerticle.java index cf8dedc..e12a958 100644 --- a/api/src/main/java/telegram/files/TelegramVerticle.java +++ b/api/src/main/java/telegram/files/TelegramVerticle.java @@ -12,10 +12,7 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.TypeUtil; import cn.hutool.log.Log; import cn.hutool.log.LogFactory; -import io.vertx.core.AbstractVerticle; -import io.vertx.core.CompositeFuture; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; +import io.vertx.core.*; import io.vertx.core.impl.NoStackTraceException; import io.vertx.core.json.Json; import io.vertx.core.json.JsonArray; @@ -24,10 +21,7 @@ import org.drinkless.tdlib.Client; import org.drinkless.tdlib.TdApi; import org.jooq.lambda.tuple.Tuple; import org.jooq.lambda.tuple.Tuple2; -import telegram.files.repository.FileRecord; -import telegram.files.repository.SettingAutoRecords; -import telegram.files.repository.SettingKey; -import telegram.files.repository.TelegramRecord; +import telegram.files.repository.*; import java.io.File; import java.io.IOError; @@ -50,6 +44,8 @@ public class TelegramVerticle extends AbstractVerticle { public String rootPath; + private String proxyName; + private String rootId; private boolean needDelete = false; @@ -74,6 +70,7 @@ public class TelegramVerticle extends AbstractVerticle { public TelegramVerticle(TelegramRecord telegramRecord) { this.telegramRecord = telegramRecord; this.rootPath = telegramRecord.rootPath(); + this.proxyName = telegramRecord.proxy(); } public String getRootId() { @@ -87,14 +84,21 @@ public class TelegramVerticle extends AbstractVerticle { return telegramRecord == null ? this.getRootId() : telegramRecord.id(); } + public void setProxy(String proxyName) { + this.proxyName = proxyName; + } + @Override - public void start() { + public void start(Promise startPromise) { TelegramUpdateHandler telegramUpdateHandler = new TelegramUpdateHandler(); telegramUpdateHandler.setOnAuthorizationStateUpdated(this::onAuthorizationStateUpdated); telegramUpdateHandler.setOnFileUpdated(this::onFileUpdated); telegramUpdateHandler.setOnFileDownloadsUpdated(this::onFileDownloadsUpdated); telegramUpdateHandler.setOnMessageReceived(this::onMessageReceived); client = Client.create(telegramUpdateHandler, this::handleException, this::handleException); + this.enableProxy(this.proxyName) + .onSuccess(r -> startPromise.complete()) + .onFailure(startPromise::fail); } public void delete() { @@ -129,7 +133,8 @@ public class TelegramVerticle extends AbstractVerticle { .put("status", "inactive") .put("rootPath", this.rootPath) .put("isPremium", false) - .put("lastAuthorizationState", lastAuthorizationState); + .put("lastAuthorizationState", lastAuthorizationState) + .put("proxy", this.proxyName); if (this.telegramRecord != null) { jsonObject.put("id", Convert.toStr(this.telegramRecord.id())) .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("status", "active") .put("rootPath", this.rootPath) - .put("isPremium", user.isPremium); + .put("isPremium", user.isPremium) + .put("proxy", this.proxyName); promise.complete(result); }) .onFailure(e -> { @@ -158,6 +164,9 @@ public class TelegramVerticle extends AbstractVerticle { public Future getChats(Long activatedChatId, String query) { Consumer insertActivatedChatId = chats -> { + if (chats == null || ArrayUtil.isEmpty(chats.chatIds)) { + return; + } long[] chatIds = chats.chatIds; if (activatedChatId != null && !ArrayUtil.contains(chatIds, activatedChatId)) { chatIds = (long[]) ArrayUtil.insert(chatIds, 0, activatedChatId); @@ -293,7 +302,9 @@ public class TelegramVerticle extends AbstractVerticle { if (fileRecord != null) { promise.complete(); } 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()) .onFailure(promise::fail); } @@ -402,6 +413,87 @@ public class TelegramVerticle extends AbstractVerticle { return DataVerticle.fileRepository.getDownloadStatistics(this.telegramRecord.id()); } + public Future enableProxy(String proxyName) { + if (StrUtil.isBlank(proxyName)) return Future.succeededFuture(); + return DataVerticle.settingRepository.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 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 getTdProxy(SettingProxyRecords.Item proxy) { + return this.execute(new TdApi.GetProxies()) + .map(proxies -> Stream.of(proxies.proxies) + .filter(proxy::equalsTdProxy) + .findFirst() + .orElse(null)); + } + + public Future getTdProxy() { + return this.execute(new TdApi.GetProxies()) + .map(proxies -> Stream.of(proxies.proxies) + .filter(p -> p.isEnabled) + .findFirst() + .orElse(null)); + } + + public Future ping() { + return this.getTdProxy() + .compose(proxy -> this.execute(new TdApi.PingProxy(proxy == null ? 0 : proxy.id))) + .map(r -> r.seconds); + } + @SuppressWarnings("unchecked") public Future execute(TdApi.Function method) { log.trace("[%s] Execute method: %s".formatted(getRootId(), TypeUtil.getTypeArgument(method.getClass()))); @@ -506,7 +598,7 @@ public class TelegramVerticle extends AbstractVerticle { if (telegramRecord == null) { this.execute(new TdApi.GetMe()) .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 -> { telegramRecord = record; diff --git a/api/src/main/java/telegram/files/repository/SettingKey.java b/api/src/main/java/telegram/files/repository/SettingKey.java index d3cd407..c49deab 100644 --- a/api/src/main/java/telegram/files/repository/SettingKey.java +++ b/api/src/main/java/telegram/files/repository/SettingKey.java @@ -15,6 +15,7 @@ public enum SettingKey { * Auto download limit for each telegram account */ autoDownloadLimit(Convert::toInt), + proxys(value -> new JsonObject(value).mapTo(SettingProxyRecords.class)), ; public final Function converter; diff --git a/api/src/main/java/telegram/files/repository/SettingProxyRecords.java b/api/src/main/java/telegram/files/repository/SettingProxyRecords.java new file mode 100644 index 0000000..2071dbc --- /dev/null +++ b/api/src/main/java/telegram/files/repository/SettingProxyRecords.java @@ -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 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 getProxy(String name) { + return items.stream().filter(item -> Objects.equals(name, item.name)).findFirst(); + } +} diff --git a/api/src/main/java/telegram/files/repository/TelegramRecord.java b/api/src/main/java/telegram/files/repository/TelegramRecord.java index c02da5e..393aa1f 100644 --- a/api/src/main/java/telegram/files/repository/TelegramRecord.java +++ b/api/src/main/java/telegram/files/repository/TelegramRecord.java @@ -4,28 +4,38 @@ import cn.hutool.core.map.MapUtil; import io.vertx.sqlclient.templates.RowMapper; import io.vertx.sqlclient.templates.TupleMapper; -import java.util.Map; - -public record TelegramRecord(long id, String firstName, String rootPath) { +public record TelegramRecord( + long id, + String firstName, + String rootPath, + String proxy +) { public static final String SCHEME = """ CREATE TABLE IF NOT EXISTS telegram_record ( id BIGINT PRIMARY KEY, first_name VARCHAR(255), - root_path VARCHAR(255) + root_path VARCHAR(255), + proxy VARCHAR(255) ) """; public static RowMapper ROW_MAPPER = row -> new TelegramRecord(row.getLong("id"), row.getString("first_name"), - row.getString("root_path") + row.getString("root_path"), + row.getString("proxy") ); public static TupleMapper PARAM_MAPPER = TupleMapper.mapper(r -> - Map.ofEntries(MapUtil.entry("id", r.id), - Map.entry("first_name", r.firstName()), - Map.entry("root_path", r.rootPath()) + MapUtil.ofEntries(MapUtil.entry("id", r.id), + MapUtil.entry("first_name", r.firstName()), + MapUtil.entry("root_path", r.rootPath()), + MapUtil.entry("proxy", r.proxy()) )); + + public TelegramRecord withProxy(String proxy) { + return new TelegramRecord(id, firstName, rootPath, proxy); + } } diff --git a/api/src/main/java/telegram/files/repository/TelegramRepository.java b/api/src/main/java/telegram/files/repository/TelegramRepository.java index 978f482..fc46007 100644 --- a/api/src/main/java/telegram/files/repository/TelegramRepository.java +++ b/api/src/main/java/telegram/files/repository/TelegramRepository.java @@ -12,6 +12,8 @@ public interface TelegramRepository { Future create(TelegramRecord telegramRecord); + Future update(TelegramRecord telegramRecord); + Future getById(long id); Future> getAll(); diff --git a/api/src/main/java/telegram/files/repository/impl/TelegramRepositoryImpl.java b/api/src/main/java/telegram/files/repository/impl/TelegramRepositoryImpl.java index 2cc1902..897f3de 100644 --- a/api/src/main/java/telegram/files/repository/impl/TelegramRepositoryImpl.java +++ b/api/src/main/java/telegram/files/repository/impl/TelegramRepositoryImpl.java @@ -47,7 +47,7 @@ public class TelegramRepositoryImpl implements TelegramRepository { @Override public Future create(TelegramRecord telegramRecord) { 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) .execute(telegramRecord) .map(r -> telegramRecord) @@ -57,6 +57,19 @@ public class TelegramRepositoryImpl implements TelegramRepository { ); } + @Override + public Future 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 public Future getById(long id) { return SqlTemplate diff --git a/request.http b/request.http index cf33284..321abdf 100644 --- a/request.http +++ b/request.http @@ -52,6 +52,10 @@ Cookie: {{tf}} POST http://{{server}}/telegram/create Cookie: {{tf}} +### Ping +GET http://{{server}}/telegram/{{telegramId}}/ping +Cookie: {{tf}} + ### Get chats GET http://{{server}}/telegram/{{telegramId}}/chats Cookie: {{tf}} diff --git a/web/src/components/account-dialog.tsx b/web/src/components/account-dialog.tsx index 365be81..86ae891 100644 --- a/web/src/components/account-dialog.tsx +++ b/web/src/components/account-dialog.tsx @@ -20,13 +20,14 @@ import { } from "@/lib/websocket-types"; import { useToast } from "@/hooks/use-toast"; 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 { cn } from "@/lib/utils"; import { type TelegramApiResult } from "@/lib/types"; import { useSWRConfig } from "swr"; import { useDebounce } from "use-debounce"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp"; +import ProxysDialog from "@/components/proxys-dialog"; export function AccountDialog({ children, @@ -37,12 +38,24 @@ export function AccountDialog({ }) { const { toast } = useToast(); const { mutate } = useSWRConfig(); + const [proxyName, setProxyName] = useState(); const { data: createData, trigger: triggerCreate, isMutating: isCreateMutating, 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 } = useSWRMutation( "/telegram/api", @@ -137,7 +150,6 @@ export function AccountDialog({ if (isMethodMutating) return true; const lastCode = methodCodes[methodCodes.length - 1]; if (!lastCode) return false; - console.log(lastCode, methodCompleteCodes); return !methodCompleteCodes.includes(lastCode); }, [isMethodMutating, methodCodes, methodCompleteCodes]); @@ -173,7 +185,7 @@ export function AccountDialog({ {children} @@ -184,7 +196,7 @@ export function AccountDialog({ {!debounceIsCreateMutating && !initSuccessfully && ( -
+
-
- +
+ +
+ + +
+
+ + +
{hasAccounts && accounts.length > 0 && onSelectAccount && ( diff --git a/web/src/components/proxy-ping.tsx b/web/src/components/proxy-ping.tsx new file mode 100644 index 0000000..577a7f4 --- /dev/null +++ b/web/src/components/proxy-ping.tsx @@ -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 ( + + + +
{ + if (isLoading) return; + void mutate(undefined, true); + }} + > + + {isLoading && ( +
+ )} + {!isLoading && error && Connection error} + {!isLoading && data && ( +
+ Ping: {(data.ping * 1000).toFixed(0)}ms +
+ )} +
+
+ + Current latency for accessing telegram API + +
+
+ ); +} diff --git a/web/src/components/proxys-dialog.tsx b/web/src/components/proxys-dialog.tsx new file mode 100644 index 0000000..e32b623 --- /dev/null +++ b/web/src/components/proxys-dialog.tsx @@ -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 ( + + + + + + + Proxys + +
+ +
+
+
+ ); +} diff --git a/web/src/components/proxys.tsx b/web/src/components/proxys.tsx new file mode 100644 index 0000000..7b0a301 --- /dev/null +++ b/web/src/components/proxys.tsx @@ -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(null); + const [formState, setFormState] = useState({ + 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, + ): 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 ( +
+
+
+

Proxys

+ {telegramId && } +
+ +
+
+ {proxys.map((proxy) => ( + { + if (innerProxyName === proxy.name) { + setInnerProxyName(""); + } else { + setInnerProxyName(proxy.name); + } + }} + > + {proxyName && proxy.name === proxyName && ( + + )} + + + {proxy.name} + + + +
+

+ {proxy.type.toUpperCase()} +

+

{`${proxy.server}:${proxy.port}`}

+
+
+ + + + +
+ ))} +
+ {enableSelect && ( + + )} + + + + + {editingProxy ? "Edit Proxy" : "Add Proxy"} + + +
+ {/* Radio buttons for proxy type */} +
+ +
+ + +
+
+ + {/* Other input fields */} +
+ + +
+
+

+ Proxy server address and port number +

+
+
+ + +
+
+ + +
+
+
+

+ Authentication (optional) +

+
+ + +
+
+ + +
+
+ + + + +
+
+
+ ); +} diff --git a/web/src/components/settings-dialog.tsx b/web/src/components/settings-dialog.tsx index 159e2db..20ec031 100644 --- a/web/src/components/settings-dialog.tsx +++ b/web/src/components/settings-dialog.tsx @@ -1,4 +1,4 @@ -import React, { type FormEvent, useState } from "react"; +import React, { useState } from "react"; import { Dialog, DialogContent, @@ -6,45 +6,17 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { - 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 { Settings } from "lucide-react"; import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; 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 { useTelegramAccount } from "@/hooks/use-telegram-account"; +import Proxys from "@/components/proxys"; +import SettingsForm from "@/components/settings-form"; export const SettingsDialog: React.FC = () => { const [isOpen, setIsOpen] = useState(false); - const { 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(); - }; + const { account, accountId } = useTelegramAccount(); return ( @@ -73,128 +45,34 @@ export const SettingsDialog: React.FC = () => { General Statistics + Proxys - -
-
-

- - These settings will be applied to all accounts. -

-
-
- - - void setSetting("uniqueOnly", String(checked)) - } - /> -
-

- Show only unique file in the table. If disabled, will show - all.
- Warning: If enabled, the number of - documents on the form will be inaccurate. -

-
-
-
- - { - void setSetting("needToLoadImages", String(checked)); - }} - /> -
- {settings?.needToLoadImages === "true" && ( -
- - -

- The size of the image to load in the browser. -

-
- )} -
-
-
- - - void setSetting("showSensitiveContent", String(checked)) - } - /> -
-

- Show sensitive content in the table, Will use a spoiler to - hide sensitive content if disabled. -

-
-
- -
- - { - void setSetting("autoDownloadLimit", e.target.value); - }} - /> -
-
-
-
- -
-
+ +
+ +
- - {accountId ? ( - - ) : ( -
-

- Please select an account to view statistics -

-
- )} + +
+ {accountId ? ( + + ) : ( +
+

+ Please select an account to view statistics +

+
+ )} +
+
+ +
+ +
diff --git a/web/src/components/settings-form.tsx b/web/src/components/settings-form.tsx new file mode 100644 index 0000000..6c81b34 --- /dev/null +++ b/web/src/components/settings-form.tsx @@ -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 ( +
+
+

+ + These settings will be applied to all accounts. +

+
+
+ + + void setSetting("uniqueOnly", String(checked)) + } + /> +
+

+ Show only unique file in the table. If disabled, will show all.{" "} +
+ Warning: If enabled, the number of documents on the + form will be inaccurate. +

+
+
+
+ + { + void setSetting("needToLoadImages", String(checked)); + }} + /> +
+ {settings?.needToLoadImages === "true" && ( +
+ + +

+ The size of the image to load in the browser. +

+
+ )} +
+
+
+ + + void setSetting("showSensitiveContent", String(checked)) + } + /> +
+

+ Show sensitive content in the table, Will use a spoiler to hide + sensitive content if disabled. +

+
+
+ +
+ + { + void setSetting("autoDownloadLimit", e.target.value); + }} + /> +
+
+
+
+ +
+
+ ); +} diff --git a/web/src/hooks/use-settings.tsx b/web/src/hooks/use-settings.tsx index 762825e..5456be7 100644 --- a/web/src/hooks/use-settings.tsx +++ b/web/src/hooks/use-settings.tsx @@ -9,7 +9,7 @@ interface SettingsContextType { isLoading: boolean; settings?: Settings; setSetting: (key: SettingKey, value: string) => Promise; - updateSettings: (updates?: Settings) => Promise; + updateSettings: (updates?: Partial) => Promise; } const SettingsContext = createContext( @@ -41,13 +41,15 @@ export const SettingsProvider: React.FC = ({ ); // 更新配置项方法 - const updateSettings = async (updates?: Settings) => { + const updateSettings = async (updates?: Partial) => { await request("/settings/create", { body: updates ? JSON.stringify(updates) : JSON.stringify(settings), method: "POST", }); // 更新缓存 if (updates) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error await mutate((current) => ({ ...current, ...updates }), false); } toast({ title: "Success", description: "Settings updated" }); diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 5d8912b..f474d25 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -7,6 +7,7 @@ export type TelegramAccount = { avatar?: string; status: "active" | "inactive"; lastAuthorizationState?: TelegramObject; + proxy?: string; }; export type TelegramChat = { @@ -85,8 +86,20 @@ export const SettingKeys = [ "imageLoadSize", "showSensitiveContent", "autoDownloadLimit", + "proxys", ] as const; export type SettingKey = (typeof SettingKeys)[number]; export type Settings = Record; + +export type Proxy = { + id?: string; + name: string; + server: string; + port: number; + username: string; + password: string; + type: "http" | "socks5"; + isEnabled?: boolean; +}