diff --git a/api/src/main/java/telegram/files/TelegramVerticle.java b/api/src/main/java/telegram/files/TelegramVerticle.java index a2a2d0b..1af2be3 100644 --- a/api/src/main/java/telegram/files/TelegramVerticle.java +++ b/api/src/main/java/telegram/files/TelegramVerticle.java @@ -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); diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 2b4fd61..ca9793a 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -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 ( 0} accounts={accounts} onSelectAccount={handleAccountChange} diff --git a/web/src/components/account-creator.tsx b/web/src/components/account-creator.tsx new file mode 100644 index 0000000..8afa4fe --- /dev/null +++ b/web/src/components/account-creator.tsx @@ -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(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 ( +
+ Initializing account, please wait + +
+ ); + } + + if (createError) { + return ( +
+ 😲 + Initializing account failed, please try again later. +
+ ); + } + + if (!initSuccessfully) { + return ( +
+ +
+ ); + } + + if (!authState && !isMethodExecuting) { + return ( +
+

Waiting for the telegram account to be initialized, please wait

+

If it takes too long, please refresh the page or try again later.

+ +
+ ); + } + + const authStateFormFields = { + [TelegramConstructor.WAIT_PHONE_NUMBER]: ( +
+ +

+ Should with country code like: 8613712345678 +

+ setPhoneNumber(e.target.value)} + disabled={isMethodExecuting} + required + /> +
+ ), + [TelegramConstructor.WAIT_CODE]: ( +
+ +

+ Please enter the code sent to your telegram account. +

+ setCode(value)} + > + + + + + + + + +
+ ), + [TelegramConstructor.WAIT_PASSWORD]: ( +
+ +

+ You have enabled two-step verification, please enter your password. +

+ setPassword(e.target.value)} + disabled={isMethodExecuting} + required + /> +
+ ), + }; + + 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 ( +
+ {authState && ( + <> + {authStateFormFields[authState]} + + + )} +
+ ); +} diff --git a/web/src/components/account-dialog.tsx b/web/src/components/account-dialog.tsx index 20bdda5..d3b6d36 100644 --- a/web/src/components/account-dialog.tsx +++ b/web/src/components/account-dialog.tsx @@ -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(); - 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( - "/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(); const [open, setOpen] = useState(false); - const [authState, setAuthState] = useState(undefined); - const [phoneNumber, setPhoneNumber] = useState(""); - const [code, setCode] = useState(""); - const [password, setPassword] = useState(""); - const { lastJsonMessage } = useWebsocket(); - - const [methodCodes, setMethodCodes] = useState([]); - const [methodCompleteCodes, setMethodCompleteCodes] = useState([]); - - 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 ( - + { + if (isAdd) { + setNewAccountId(undefined); + setProxyName(undefined); + } + setOpen(open); + }} + > {children} Add Telegram Account -

- {account ? account.id : createData?.id} -

+ {(account ?? newAccountId) && ( +

+ {account ? account.id : newAccountId} +

+ )}
- {!debounceIsCreateMutating && !initSuccessfully && ( -
- -
- )} - {debounceIsCreateMutating && ( -
- Initializing account, please wait - -
- )} - {!debounceIsCreateMutating && createError && ( -
- 😲 - Initializing account failed, please try again later. -
- )} - {!debounceIsCreateMutating && initSuccessfully && ( -
- {authState === TelegramConstructor.WAIT_PHONE_NUMBER && ( -
- -

- Should with country code like: 8613712345678 -

- setPhoneNumber(e.target.value)} - disabled={isMethodExecuting} - required - /> -
- )} - {authState === TelegramConstructor.WAIT_CODE && ( -
- -

- Please enter the code sent to your telegram account. -

- setCode(value)} - > - - - - - - - - -
- )} - {authState === TelegramConstructor.WAIT_PASSWORD && ( -
- -

- You have enabled two-step verification, please enter your - password. -

- setPassword(e.target.value)} - disabled={isMethodExecuting} - required - /> -
- )} - {authState && ( - - )} -
- )} + setOpen(false)} + />
+ {isLoadingAccount && ( +
+ +
+ )} + {hasAccounts && accounts.length > 0 && onSelectAccount && ( )} diff --git a/web/src/components/proxys.tsx b/web/src/components/proxys.tsx index 2b5bdd7..d016de9 100644 --- a/web/src/components/proxys.tsx +++ b/web/src/components/proxys.tsx @@ -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({ Add Proxy + {proxys.length === 0 && ( +
+

No proxys added yet

+
+ )}
{proxys.map((proxy) => ( - {editingProxy ? "Edit Proxy" : "Add Proxy"} + + {editingProxy ? "Edit Proxy" : "Add Proxy"} + + setFormState(proxy)} />
@@ -347,3 +361,51 @@ export default function Proxys({
); } + +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 ( + + + + + + +
+

+ Parse the proxy string from the clipboard and add them to the list +

+

+ The proxy should be in the following format: +
+ http://username:password@server:port +
+ socks://username:password@server:port +

+
+
+
+
+ ); +} diff --git a/web/src/hooks/use-telegram-method.tsx b/web/src/hooks/use-telegram-method.tsx new file mode 100644 index 0000000..0aaf53e --- /dev/null +++ b/web/src/hooks/use-telegram-method.tsx @@ -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([]); + const [methodCompleteCodes, setMethodCompleteCodes] = useState([]); + const { lastJsonMessage } = useWebsocket(); + + const { trigger: triggerMethod, isMutating: isMethodMutating } = + useSWRMutation( + "/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, + }; +} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 365058c..9c6b3a9 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -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", + }; +}