♻️ refactor: 1. Refactor the account creation code 2. Add the function of parsing the proxy from the clipboard

This commit is contained in:
jarvis2f 2025-01-11 13:45:27 +08:00
parent e1c2105501
commit 1039099d04
8 changed files with 471 additions and 283 deletions

View file

@ -553,7 +553,13 @@ public class TelegramVerticle extends AbstractVerticle {
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 -> {
if (this.telegramRecord != null) {
return DataVerticle.telegramRepository.update(this.telegramRecord.withProxy(proxyName)).map(r);
} else {
return Future.succeededFuture(r);
}
})
.compose(r -> {
this.proxyName = proxyName;
return Future.succeededFuture(r);

View file

@ -4,10 +4,11 @@ import { EmptyState } from "@/components/empty-state";
import { useTelegramAccount } from "@/hooks/use-telegram-account";
export default function Home() {
const { getAccounts, handleAccountChange } = useTelegramAccount();
const { getAccounts, handleAccountChange, isLoading } = useTelegramAccount();
const accounts = getAccounts();
return (
<EmptyState
isLoadingAccount={isLoading}
hasAccounts={(accounts ?? []).length > 0}
accounts={accounts}
onSelectAccount={handleAccountChange}

View file

@ -0,0 +1,287 @@
import { useTelegramMethod } from "@/hooks/use-telegram-method";
import useSWRMutation from "swr/mutation";
import { request } from "@/lib/api";
import { useToast } from "@/hooks/use-toast";
import { useSWRConfig } from "swr";
import { type FormEvent, useCallback, useEffect, useState } from "react";
import { useDebounce } from "use-debounce";
import { Ellipsis, LoaderCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
TelegramConstructor,
type TelegramObject,
WebSocketMessageType,
} from "@/lib/websocket-types";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { useWebsocket } from "@/hooks/use-websocket";
import { useTelegramAccount } from "@/hooks/use-telegram-account";
interface AccountCreatorProps {
isAdd?: boolean;
proxyName: string | undefined;
onCreated?: (id: string) => void;
onLoginSuccess?: () => void;
}
export default function AccountCreator({
isAdd,
proxyName,
onCreated,
onLoginSuccess,
}: AccountCreatorProps) {
const { triggerMethod, isMethodExecuting } = useTelegramMethod();
const { toast } = useToast();
const { mutate } = useSWRConfig();
const { lastJsonMessage } = useWebsocket();
const { account, resetAccount } = useTelegramAccount();
const [initSuccessfully, setInitSuccessfully] = useState(false);
const [authState, setAuthState] = useState<number | undefined>(undefined);
const [phoneNumber, setPhoneNumber] = useState("");
const [code, setCode] = useState("");
const [password, setPassword] = useState("");
const {
trigger: triggerCreate,
isMutating: isCreateMutating,
error: createError,
} = 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,
}),
});
},
{
onSuccess: (data) => {
onCreated?.(data.id);
},
},
);
const [debounceIsCreateMutating] = useDebounce(isCreateMutating, 1000, {
leading: true,
});
const handleAuthState = useCallback(
(state: TelegramObject) => {
switch (state.constructor) {
case TelegramConstructor.WAIT_PHONE_NUMBER:
case TelegramConstructor.WAIT_CODE:
case TelegramConstructor.WAIT_PASSWORD:
setAuthState(state.constructor);
break;
case TelegramConstructor.STATE_READY:
toast({
title: "Success",
description: "Account added successfully",
});
setTimeout(() => {
void mutate("/telegrams");
onLoginSuccess?.();
setPhoneNumber("");
setCode("");
setPassword("");
}, 1000);
break;
default:
console.log("Unknown telegram constructor:", state.constructor);
}
},
[mutate, onLoginSuccess, toast],
);
useEffect(() => {
if (account) {
if (
!isAdd &&
account.status === "inactive" &&
account.lastAuthorizationState
) {
setInitSuccessfully(true);
handleAuthState(account.lastAuthorizationState);
return;
}
}
if (isAdd && !initSuccessfully) {
resetAccount();
}
}, [account, handleAuthState, initSuccessfully, isAdd, resetAccount]);
useEffect(() => {
if (!lastJsonMessage) return;
if (lastJsonMessage.type === WebSocketMessageType.AUTHORIZATION) {
handleAuthState(lastJsonMessage.data as TelegramObject);
}
}, [handleAuthState, lastJsonMessage]);
useEffect(() => {
if (phoneNumber) {
setPhoneNumber((prev) => prev.replaceAll(/\D/g, ""));
}
}, [phoneNumber]);
if (debounceIsCreateMutating) {
return (
<div className="flex items-center justify-center space-x-2 text-xl">
<span>Initializing account, please wait</span>
<Ellipsis className="h-4 w-4 animate-pulse" />
</div>
);
}
if (createError) {
return (
<div className="text-center text-xl">
<span className="mr-3 text-3xl">😲</span>
Initializing account failed, please try again later.
</div>
);
}
if (!initSuccessfully) {
return (
<div className="flex flex-col items-center justify-center space-y-4">
<Button
className={cn("w-full", debounceIsCreateMutating ? "opacity-50" : "")}
disabled={debounceIsCreateMutating}
onClick={async () => {
await triggerCreate().then(() => {
void mutate("/telegrams");
setInitSuccessfully(true);
});
}}
>
Start Initialization
</Button>
</div>
);
}
if (!authState && !isMethodExecuting) {
return (
<div className="flex items-center justify-center space-x-2 text-xl">
<p>Waiting for the telegram account to be initialized, please wait</p>
<p>If it takes too long, please refresh the page or try again later.</p>
<Ellipsis className="h-4 w-4 animate-pulse" />
</div>
);
}
const authStateFormFields = {
[TelegramConstructor.WAIT_PHONE_NUMBER]: (
<div className="space-y-2">
<Label htmlFor="phone">Phone Number</Label>
<p className="text-xs text-gray-500">
Should with country code like: 8613712345678
</p>
<Input
id="phone"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
disabled={isMethodExecuting}
required
/>
</div>
),
[TelegramConstructor.WAIT_CODE]: (
<div className="space-y-2">
<Label htmlFor="code">Authentication Code</Label>
<p className="text-xs text-gray-500">
Please enter the code sent to your telegram account.
</p>
<InputOTP
id="code"
maxLength={5}
value={code}
disabled={isMethodExecuting}
required
onChange={(value) => setCode(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
</InputOTPGroup>
</InputOTP>
</div>
),
[TelegramConstructor.WAIT_PASSWORD]: (
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<p className="text-xs text-gray-500">
You have enabled two-step verification, please enter your password.
</p>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isMethodExecuting}
required
/>
</div>
),
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (authState === TelegramConstructor.WAIT_PHONE_NUMBER) {
await triggerMethod({
data: {
phoneNumber: phoneNumber,
settings: null,
},
method: "SetAuthenticationPhoneNumber",
});
} else if (authState === TelegramConstructor.WAIT_CODE) {
await triggerMethod({
data: {
code: code,
},
method: "CheckAuthenticationCode",
});
} else if (authState === TelegramConstructor.WAIT_PASSWORD) {
await triggerMethod({
data: {
password: password,
},
method: "CheckAuthenticationPassword",
});
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
{authState && (
<>
{authStateFormFields[authState]}
<Button
type="submit"
className={cn("w-full", isMethodExecuting ? "opacity-50" : "")}
disabled={isMethodExecuting}
>
{isMethodExecuting ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
"🚀 Submit"
)}
</Button>
</>
)}
</form>
);
}

View file

@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { type ReactNode, useState } from "react";
import {
Dialog,
DialogContent,
@ -8,180 +8,33 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Ellipsis, LoaderCircle } from "lucide-react";
import { useWebsocket } from "@/hooks/use-websocket";
import {
TelegramConstructor,
type TelegramObject,
WebSocketMessageType,
} from "@/lib/websocket-types";
import { useToast } from "@/hooks/use-toast";
import useSWRMutation from "swr/mutation";
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";
import AccountCreator from "@/components/account-creator";
export function AccountDialog({
children,
isAdd,
}: {
children: React.ReactNode;
children: ReactNode;
isAdd?: boolean;
}) {
const { toast } = useToast();
const { mutate } = useSWRConfig();
const [proxyName, setProxyName] = useState<string | undefined>();
const {
data: createData,
trigger: triggerCreate,
isMutating: isCreateMutating,
error: createError,
} = 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<TelegramApiResult, Error, string, TelegramApiArg>(
"/telegram/api",
telegramApi,
{
onSuccess: (data) => {
setMethodCodes((prev) => [...prev, data.code]);
},
},
);
const { account, resetAccount } = useTelegramAccount();
const [initSuccessfully, setInitSuccessfully] = useState(false);
const { account } = useTelegramAccount();
const [newAccountId, setNewAccountId] = useState<string | undefined>();
const [open, setOpen] = useState(false);
const [authState, setAuthState] = useState<number | undefined>(undefined);
const [phoneNumber, setPhoneNumber] = useState("");
const [code, setCode] = useState("");
const [password, setPassword] = useState("");
const { lastJsonMessage } = useWebsocket();
const [methodCodes, setMethodCodes] = useState<string[]>([]);
const [methodCompleteCodes, setMethodCompleteCodes] = useState<string[]>([]);
const [debounceIsCreateMutating] = useDebounce(isCreateMutating, 1000, {
leading: true,
});
const handleAuthState = useCallback(
(state: TelegramObject) => {
switch (state.constructor) {
case TelegramConstructor.WAIT_PHONE_NUMBER:
case TelegramConstructor.WAIT_CODE:
case TelegramConstructor.WAIT_PASSWORD:
setAuthState(state.constructor);
break;
case TelegramConstructor.STATE_READY:
toast({
title: "Success",
description: "Account added successfully",
});
setTimeout(() => {
void mutate("/telegrams");
setOpen(false);
setPhoneNumber("");
setCode("");
setPassword("");
}, 1000);
break;
default:
console.log("Unknown telegram constructor:", state.constructor);
}
},
[mutate, toast],
);
useEffect(() => {
if (account) {
if (
!isAdd &&
account.status === "inactive" &&
account.lastAuthorizationState
) {
setInitSuccessfully(true);
handleAuthState(account.lastAuthorizationState);
return;
}
}
if (open && isAdd && !initSuccessfully) {
resetAccount();
}
}, [account, handleAuthState, initSuccessfully, isAdd, open, resetAccount]);
useEffect(() => {
if (phoneNumber) {
setPhoneNumber((prev) => prev.replaceAll(/\D/g, ""));
}
}, [phoneNumber]);
useEffect(() => {
if (!lastJsonMessage) return;
if (lastJsonMessage.code) {
setMethodCompleteCodes((prev) => [...prev, lastJsonMessage.code]);
}
if (lastJsonMessage.type === WebSocketMessageType.AUTHORIZATION) {
handleAuthState(lastJsonMessage.data as TelegramObject);
}
}, [handleAuthState, lastJsonMessage, toast]);
const isMethodExecuting = useMemo(() => {
if (isMethodMutating) return true;
const lastCode = methodCodes[methodCodes.length - 1];
if (!lastCode) return false;
return !methodCompleteCodes.includes(lastCode);
}, [isMethodMutating, methodCodes, methodCompleteCodes]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (authState === TelegramConstructor.WAIT_PHONE_NUMBER) {
await triggerMethod({
data: {
phoneNumber: phoneNumber,
settings: null,
},
method: "SetAuthenticationPhoneNumber",
});
} else if (authState === TelegramConstructor.WAIT_CODE) {
await triggerMethod({
data: {
code: code,
},
method: "CheckAuthenticationCode",
});
} else if (authState === TelegramConstructor.WAIT_PASSWORD) {
await triggerMethod({
data: {
password: password,
},
method: "CheckAuthenticationPassword",
});
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog
open={open}
onOpenChange={(open) => {
if (isAdd) {
setNewAccountId(undefined);
setProxyName(undefined);
}
setOpen(open);
}}
>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
aria-describedby={undefined}
@ -190,125 +43,22 @@ export function AccountDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Add Telegram Account
<p className="rounded-md bg-gray-100 p-1 text-xs text-muted-foreground">
{account ? account.id : createData?.id}
</p>
{(account ?? newAccountId) && (
<p className="rounded-md bg-gray-100 p-1 text-xs text-muted-foreground">
{account ? account.id : newAccountId}
</p>
)}
</DialogTitle>
</DialogHeader>
<div className="relative h-full w-full">
{!debounceIsCreateMutating && !initSuccessfully && (
<div className="flex flex-col items-center justify-center space-y-4">
<Button
className={cn(
"w-full",
debounceIsCreateMutating ? "opacity-50" : "",
)}
disabled={debounceIsCreateMutating}
onClick={async () => {
await triggerCreate().then(() => {
void mutate("/telegrams");
setInitSuccessfully(true);
});
}}
>
{debounceIsCreateMutating ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
"Start Initialization"
)}
</Button>
</div>
)}
{debounceIsCreateMutating && (
<div className="flex items-center justify-center space-x-2 text-xl">
<span>Initializing account, please wait</span>
<Ellipsis className="h-4 w-4 animate-pulse" />
</div>
)}
{!debounceIsCreateMutating && createError && (
<div className="text-center text-xl">
<span className="mr-3 text-3xl">😲</span>
Initializing account failed, please try again later.
</div>
)}
{!debounceIsCreateMutating && initSuccessfully && (
<form onSubmit={handleSubmit} className="space-y-4">
{authState === TelegramConstructor.WAIT_PHONE_NUMBER && (
<div className="space-y-2">
<Label htmlFor="phone">Phone Number</Label>
<p className="text-xs text-gray-500">
Should with country code like: 8613712345678
</p>
<Input
id="phone"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
disabled={isMethodExecuting}
required
/>
</div>
)}
{authState === TelegramConstructor.WAIT_CODE && (
<div className="space-y-2">
<Label htmlFor="code">Authentication Code</Label>
<p className="text-xs text-gray-500">
Please enter the code sent to your telegram account.
</p>
<InputOTP
id="code"
maxLength={5}
value={code}
disabled={isMethodExecuting}
required
onChange={(value) => setCode(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
</InputOTPGroup>
</InputOTP>
</div>
)}
{authState === TelegramConstructor.WAIT_PASSWORD && (
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<p className="text-xs text-gray-500">
You have enabled two-step verification, please enter your
password.
</p>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isMethodExecuting}
required
/>
</div>
)}
{authState && (
<Button
type="submit"
className={cn(
"w-full",
isMethodExecuting ? "opacity-50" : "",
)}
disabled={isMethodExecuting}
>
{isMethodExecuting ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
"🚀 Submit"
)}
</Button>
)}
</form>
)}
<AccountCreator
isAdd={isAdd}
proxyName={proxyName}
onCreated={setNewAccountId}
onLoginSuccess={() => setOpen(false)}
/>
<ProxysDialog
telegramId={createData?.id}
telegramId={newAccountId}
proxyName={proxyName}
onProxyNameChange={setProxyName}
enableSelect={true}

View file

@ -1,4 +1,4 @@
import { MessageSquare, UserPlus } from "lucide-react";
import { LoaderPinwheel, MessageSquare, UserPlus } from "lucide-react";
import { AccountList } from "./account-list";
import { type TelegramAccount } from "@/lib/types";
import TelegramIcon from "@/components/telegram-icon";
@ -9,6 +9,7 @@ import { BorderBeam } from "@/components/ui/border-beam";
import ProxysDialog from "@/components/proxys-dialog";
interface EmptyStateProps {
isLoadingAccount?: boolean;
hasAccounts: boolean;
accounts?: TelegramAccount[];
message?: string;
@ -16,6 +17,7 @@ interface EmptyStateProps {
}
export function EmptyState({
isLoadingAccount,
hasAccounts,
accounts = [],
message,
@ -71,6 +73,15 @@ export function EmptyState({
</div>
</div>
{isLoadingAccount && (
<div className="absolute inset-0 flex items-center justify-center">
<LoaderPinwheel
className="h-8 w-8 animate-spin"
style={{ strokeWidth: "0.8px" }}
/>
</div>
)}
{hasAccounts && accounts.length > 0 && onSelectAccount && (
<AccountList accounts={accounts} onSelectAccount={onSelectAccount} />
)}

View file

@ -17,15 +17,21 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Edit2, Plus, Trash2 } from "lucide-react";
import { Copy, 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 { cn, parseProxyString } 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";
import { Label } from "./ui/label";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export interface ProxysProps {
enableSelect?: boolean;
@ -160,6 +166,11 @@ export default function Proxys({
<Plus className="mr-2 h-5 w-5" /> Add Proxy
</Button>
</div>
{proxys.length === 0 && (
<div className="flex h-32 items-center justify-center">
<p className="text-center text-gray-500">No proxys added yet</p>
</div>
)}
<div className="grid grid-cols-1 gap-6 md:grid-cols-3 2xl:grid-cols-4">
{proxys.map((proxy) => (
<Card
@ -226,7 +237,10 @@ export default function Proxys({
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingProxy ? "Edit Proxy" : "Add Proxy"}
<span className="mr-2">
{editingProxy ? "Edit Proxy" : "Add Proxy"}
</span>
<ProxyParser onParsed={(proxy) => setFormState(proxy)} />
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
@ -347,3 +361,51 @@ export default function Proxys({
</div>
);
}
function ProxyParser({ onParsed }: { onParsed: (proxys: Proxy) => void }) {
const handleParseProxy = async () => {
const clipboardText = await navigator.clipboard.readText();
if (!clipboardText || clipboardText.trim().length === 0) {
return;
}
const proxy = parseProxyString(clipboardText);
if (proxy) {
toast({
title: "Success",
description: "Proxy string is parsed successfully",
});
onParsed(proxy);
} else {
toast({
title: "Error",
description: "Invalid proxy string format",
});
}
};
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="xs" variant="ghost" onClick={() => handleParseProxy()}>
<Copy className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="flex flex-col space-y-2 p-2">
<p className="text-sm font-semibold">
Parse the proxy string from the clipboard and add them to the list
</p>
<p className="text-xs text-gray-500">
The proxy should be in the following format:
<br />
<code>http://username:password@server:port</code>
<br />
<code>socks://username:password@server:port</code>
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View file

@ -0,0 +1,41 @@
import useSWRMutation from "swr/mutation";
import type { TelegramApiResult } from "@/lib/types";
import { telegramApi, type TelegramApiArg } from "@/lib/api";
import { useEffect, useMemo, useState } from "react";
import { useWebsocket } from "@/hooks/use-websocket";
export function useTelegramMethod() {
const [methodCodes, setMethodCodes] = useState<string[]>([]);
const [methodCompleteCodes, setMethodCompleteCodes] = useState<string[]>([]);
const { lastJsonMessage } = useWebsocket();
const { trigger: triggerMethod, isMutating: isMethodMutating } =
useSWRMutation<TelegramApiResult, Error, string, TelegramApiArg>(
"/telegram/api",
telegramApi,
{
onSuccess: (data) => {
setMethodCodes((prev) => [...prev, data.code]);
},
},
);
useEffect(() => {
if (!lastJsonMessage) return;
if (lastJsonMessage.code) {
setMethodCompleteCodes((prev) => [...prev, lastJsonMessage.code]);
}
}, [lastJsonMessage]);
const isMethodExecuting = useMemo(() => {
if (isMethodMutating) return true;
const lastCode = methodCodes[methodCodes.length - 1];
if (!lastCode) return false;
return !methodCompleteCodes.includes(lastCode);
}, [isMethodMutating, methodCodes, methodCompleteCodes]);
return {
triggerMethod,
isMethodExecuting,
};
}

View file

@ -1,6 +1,36 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { type Proxy } from "@/lib/types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function parseProxyString(proxyString: string): Proxy | null {
const proxyRegex = /^(http|socks|socks5):\/\/(([^:]+):([^@]+)@)?([^:]+):(\d+)$/i;
const match = proxyRegex.exec(proxyString);
if (!match) {
return null;
}
let type = match[1];
if (type === "http") {
type = "http";
} else {
type = "socks5";
}
const username = match[3] ?? "";
const password = match[4] ?? "";
const server = match[5] ?? "";
const port = parseInt(match[6] ?? "0", 10);
return {
name: `${type} proxy`,
server,
port,
username,
password,
type: type as "http" | "socks5",
};
}