diff --git a/packages/repl/src/components/settings/AccountsTab.tsx b/packages/repl/src/components/settings/AccountsTab.tsx index db69fef..d74d4a3 100644 --- a/packages/repl/src/components/settings/AccountsTab.tsx +++ b/packages/repl/src/components/settings/AccountsTab.tsx @@ -1,24 +1,26 @@ import type { DropdownMenuTriggerProps } from '@kobalte/core/dropdown-menu' import type { TooltipTriggerProps } from '@kobalte/core/tooltip' -import type { TelegramAccount } from 'mtcute-repl-worker/client' +import type { CustomApiFields, TelegramAccount } from 'mtcute-repl-worker/client' import type { LoginStep } from './login/Login.tsx' import { timers, unknownToError } from '@fuman/utils' import { LucideBot, + LucideChevronDown, LucideChevronRight, LucideEllipsis, + LucideFlaskConical, LucideFolderUp, LucideLogIn, - LucidePlus, LucideRefreshCw, LucideSearch, + LucideServerCog, LucideTrash, LucideTriangleAlert, LucideUser, LucideX, } from 'lucide-solid' -import { workerInvoke } from 'mtcute-repl-worker/client' +import { workerInvoke } from 'mtcute-repl-worker/client' import { nanoid } from 'nanoid' import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from 'solid-js' import { toast } from 'solid-sonner' @@ -36,11 +38,13 @@ import { useStore } from '../../store/use-store.ts' import { AccountAvatar } from '../AccountAvatar.tsx' import { ImportDropdown } from './import/ImportDropdown.tsx' import { StringSessionDefs } from './import/StringSessionImportDialog.tsx' +import { CustomApiDialog } from './login/CustomApiDialog.tsx' import { LoginForm } from './login/Login.tsx' function AddAccountDialog(props: { show: boolean testMode: boolean + apiOptions?: CustomApiFields onClose: () => void }) { const [accountId, setAccountId] = createSignal(undefined) @@ -81,6 +85,7 @@ function AddAccountDialog(props: { await workerInvoke('telegram', 'createClient', { accountId: accountId()!, testMode: props.testMode, + apiOptions: props.apiOptions, }) })) @@ -281,17 +286,71 @@ function AccountRow(props: { ) } +type AddAccountMode = 'test' | 'custom-api' | 'normal' +function LoginButton(props: { + size: 'xs' | 'sm' + onAddAccount: (mode: AddAccountMode) => void +}) { + const [showDropdown, setShowDropdown] = createSignal(false) + + return ( +
+ + + + + + + props.onAddAccount('test')}> + + Use test server + + props.onAddAccount('custom-api')}> + + Use custom API + + + +
+ ) +} + export function AccountsTab() { const accounts = useStore($accounts) const activeAccountId = useStore($activeAccountId) const [showAddAccount, setShowAddAccount] = createSignal(false) const [addAccountTestMode, setAddAccountTestMode] = createSignal(false) + const [addAccountOptions, setAddAccountOptions] = createSignal() const [searchQuery, setSearchQuery] = createSignal('') - function handleAddAccount(e: MouseEvent) { + const [showCustomApi, setShowCustomApi] = createSignal(false) + + function handleAddAccount(mode: AddAccountMode) { + if (mode === 'custom-api') { + setShowCustomApi(true) + return + } + setShowAddAccount(true) - setAddAccountTestMode(e.ctrlKey || e.metaKey) + setAddAccountTestMode(mode === 'test') + setAddAccountOptions(undefined) } const filteredAccounts = createMemo(() => { @@ -329,14 +388,7 @@ export function AccountsTab() { No accounts yet
- +
@@ -363,15 +415,7 @@ export function AccountsTab() { - - +
@@ -391,8 +435,19 @@ export function AccountsTab() { setShowAddAccount(false)} /> + + { + setShowCustomApi(false) + setAddAccountOptions(options) + setShowAddAccount(true) + }} + /> ) } diff --git a/packages/repl/src/components/settings/login/CustomApiDialog.tsx b/packages/repl/src/components/settings/login/CustomApiDialog.tsx new file mode 100644 index 0000000..7fa51c1 --- /dev/null +++ b/packages/repl/src/components/settings/login/CustomApiDialog.tsx @@ -0,0 +1,176 @@ +import type { CustomApiFields } from 'mtcute-repl-worker/client' +import type { SetStoreFunction } from 'solid-js/store' +import { createSignal, Show } from 'solid-js' +import { createStore, unwrap } from 'solid-js/store' +import { Button } from '../../../lib/components/ui/button.tsx' +import { Checkbox, CheckboxControl, CheckboxLabel } from '../../../lib/components/ui/checkbox.tsx' +import { Dialog, DialogContent, DialogHeader } from '../../../lib/components/ui/dialog.tsx' +import { TextField, TextFieldFrame, TextFieldLabel, TextFieldRoot } from '../../../lib/components/ui/text-field.tsx' + +export function useCustomApiFormState() { + // eslint-disable-next-line solid/reactivity + return createStore({ + apiId: '', + apiHash: '', + deviceModel: '', + systemVersion: '', + appVersion: '', + systemLangCode: '', + langPack: '', + langCode: '', + extraJson: '', + }) +} + +export function CustomApiForm(props: { + class?: string + state: CustomApiFields + setState: SetStoreFunction +}) { + const [showAdvanced, setShowAdvanced] = createSignal(false) + + return ( +
+ + API ID + + props.setState('apiId', e.currentTarget.value.replace(/\D/g, ''))} + /> + + + + API Hash + + props.setState('apiHash', e.currentTarget.value)} + /> + + + + + + + Show advanced fields + + + + +
+ + Device model + + props.setState('deviceModel', e.currentTarget.value)} + /> + + + + + Language pack + + props.setState('langPack', e.currentTarget.value)} + /> + + +
+
+ + System version + + props.setState('systemVersion', e.currentTarget.value)} + /> + + + + App version + + props.setState('appVersion', e.currentTarget.value)} + /> + + +
+
+ + System language code + + props.setState('systemLangCode', e.currentTarget.value)} + /> + + + + Language code + + props.setState('langCode', e.currentTarget.value)} + /> + + +
+ + Extra options (JSON) + + props.setState('extraJson', e.currentTarget.value)} + /> + + +
+
+ ) +} + +export function CustomApiDialog(props: { + visible: boolean + setVisible: (visible: boolean) => void + onSubmit: (options: CustomApiFields) => void +}) { + const [state, setState] = useCustomApiFormState() + + return ( + + + + Custom connection options + + + + + + + ) +} diff --git a/packages/repl/src/components/settings/login/Login.tsx b/packages/repl/src/components/settings/login/Login.tsx index c55889b..ae5ac57 100644 --- a/packages/repl/src/components/settings/login/Login.tsx +++ b/packages/repl/src/components/settings/login/Login.tsx @@ -4,6 +4,7 @@ import { unknownToError } from '@fuman/utils' import { LucideChevronRight, LucideLockKeyhole, MessageSquareMore } from 'lucide-solid' import { workerInvoke, workerOn } from 'mtcute-repl-worker/client' import { createSignal, For, Match, onCleanup, onMount, Show, Switch } from 'solid-js' +import { toast } from 'solid-sonner' import { Button } from '../../../lib/components/ui/button.tsx' import { OTPField, OTPFieldGroup, OTPFieldInput, OTPFieldSlot } from '../../../lib/components/ui/otp-field.tsx' import { Spinner } from '../../../lib/components/ui/spinner.tsx' @@ -55,14 +56,18 @@ function QrLoginStep(props: StepProps<'qr'>) { cleanup2() }) - const result = await workerInvoke('telegram', 'signInQr', { - accountId: props.accountId, - abortSignal: abortController.signal, - }) - if (result === 'need_password') { - props.setStep('password') - } else { - props.setStep('done', { account: result }) + try { + const result = await workerInvoke('telegram', 'signInQr', { + accountId: props.accountId, + abortSignal: abortController.signal, + }) + if (result === 'need_password') { + props.setStep('password') + } else { + props.setStep('done', { account: result }) + } + } catch (e) { + toast.error(unknownToError(e).message) } }) onCleanup(() => abortController.abort()) diff --git a/packages/repl/src/lib/components/ui/text-field.tsx b/packages/repl/src/lib/components/ui/text-field.tsx index 0c271b1..b4638bf 100644 --- a/packages/repl/src/lib/components/ui/text-field.tsx +++ b/packages/repl/src/lib/components/ui/text-field.tsx @@ -131,7 +131,7 @@ export function TextField(props: Polymorphic return ( ) diff --git a/packages/worker/src/client.ts b/packages/worker/src/client.ts index 97f4e9b..555e12a 100644 --- a/packages/worker/src/client.ts +++ b/packages/worker/src/client.ts @@ -3,7 +3,7 @@ import type { ReplWorker } from './worker/main.ts' import type { ReplWorkerEvents } from './worker/utils.ts' import { Deferred, unknownToError } from '@fuman/utils' -export type { TelegramAccount } from './store/accounts.ts' +export type { CustomApiFields, TelegramAccount } from './store/accounts.ts' export type { StringSessionLibName } from './worker/telegram.ts' // eslint-disable-next-line ts/no-namespace diff --git a/packages/worker/src/store/accounts.ts b/packages/worker/src/store/accounts.ts index decbe87..5bcf419 100644 --- a/packages/worker/src/store/accounts.ts +++ b/packages/worker/src/store/accounts.ts @@ -9,6 +9,19 @@ export interface TelegramAccount { testMode: boolean telegramId: number dcId: number + apiOptions?: CustomApiFields +} + +export interface CustomApiFields { + apiId: string + apiHash: string + deviceModel: string + systemVersion: string + appVersion: string + systemLangCode: string + langPack: string + langCode: string + extraJson: string } const AccountSchema = v.object({ @@ -19,6 +32,17 @@ const AccountSchema = v.object({ name: v.string(), testMode: v.boolean(), dcId: v.number(), + apiOptions: v.object({ + apiId: v.string(), + apiHash: v.string(), + deviceModel: v.string(), + systemVersion: v.string(), + appVersion: v.string(), + systemLangCode: v.string(), + langPack: v.string(), + langCode: v.string(), + extraJson: v.string(), + }).optional(), }) export const $accounts = persistentAtom('repl:accounts', [], { diff --git a/packages/worker/src/utils/telegram.ts b/packages/worker/src/utils/telegram.ts index a6b2804..2a3961e 100644 --- a/packages/worker/src/utils/telegram.ts +++ b/packages/worker/src/utils/telegram.ts @@ -1,17 +1,49 @@ -import type { InputStringSessionData } from '@mtcute/web/utils.js' -import type { TelegramAccount } from '../store/accounts.ts' +import type { tl } from '@mtcute/web' +import type { CustomApiFields, TelegramAccount } from '../store/accounts.ts' import { asNonNull } from '@fuman/utils' import { BaseTelegramClient, IdbStorage, TransportError } from '@mtcute/web' import { getMe } from '@mtcute/web/methods.js' +import { type InputStringSessionData, jsonToTlJson } from '@mtcute/web/utils.js' import { nanoid } from 'nanoid' -export function createInternalClient(accountId: string, testMode?: boolean) { +export function createInternalClient( + accountId: string, + testMode?: boolean, + apiOptions?: CustomApiFields, +) { + let initConnectionOptions: Partial | undefined + if (apiOptions) { + initConnectionOptions = {} + if (apiOptions.deviceModel) { + initConnectionOptions.deviceModel = apiOptions.deviceModel + } + if (apiOptions.systemVersion) { + initConnectionOptions.systemVersion = apiOptions.systemVersion + } + if (apiOptions.appVersion) { + initConnectionOptions.appVersion = apiOptions.appVersion + } + if (apiOptions.langCode) { + initConnectionOptions.langCode = apiOptions.langCode + } + if (apiOptions.langPack) { + initConnectionOptions.langPack = apiOptions.langPack + } + if (apiOptions.systemLangCode) { + initConnectionOptions.systemLangCode = apiOptions.systemLangCode + } + if (apiOptions.extraJson) { + initConnectionOptions.params = jsonToTlJson(JSON.parse(apiOptions.extraJson)) + } + } + return new BaseTelegramClient({ - apiId: Number(import.meta.env.VITE_API_ID), - apiHash: import.meta.env.VITE_API_HASH, + apiId: Number(apiOptions?.apiId ?? import.meta.env.VITE_API_ID), + apiHash: apiOptions?.apiHash ?? import.meta.env.VITE_API_HASH, storage: new IdbStorage(`mtcute:${accountId}`), testMode, logLevel: import.meta.env.DEV ? 5 : 2, + initConnectionOptions, }) } diff --git a/packages/worker/src/worker/telegram.ts b/packages/worker/src/worker/telegram.ts index 11c589f..90ed416 100644 --- a/packages/worker/src/worker/telegram.ts +++ b/packages/worker/src/worker/telegram.ts @@ -1,6 +1,6 @@ import type { BaseTelegramClient, SentCode, User } from '@mtcute/web' import type { StringSessionData } from '@mtcute/web/utils.js' -import type { TelegramAccount } from '../store/accounts.ts' +import type { CustomApiFields, TelegramAccount } from '../store/accounts.ts' import { assert, hex } from '@fuman/utils' import { DC_MAPPING_PROD, DC_MAPPING_TEST } from '@mtcute/convert' import { tl } from '@mtcute/web' @@ -31,7 +31,9 @@ function getClient(accountId: string) { function getTmpClient(accountId: string): [BaseTelegramClient, () => Promise] { const client = clients.get(accountId) if (!client) { - const tmpClient = createInternalClient(accountId) + const accountInfo = $accounts.get().find(it => it.id === accountId) + if (!accountInfo) throw new Error('Account not found') + const tmpClient = createInternalClient(accountId, accountInfo.testMode, accountInfo.apiOptions) return [tmpClient, () => tmpClient.close()] } else { return [client, () => Promise.resolve()] @@ -73,8 +75,9 @@ export class ReplWorkerTelegram { async createClient(params: { accountId: string testMode?: boolean + apiOptions?: CustomApiFields }) { - const client = createInternalClient(params.accountId, params.testMode) + const client = createInternalClient(params.accountId, params.testMode, params.apiOptions) clients.set(params.accountId, client) }