@uniquedivine/tg-sai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/profiles.ts ADDED
@@ -0,0 +1,726 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
2
+ import { homedir, platform as osPlatform } from "node:os"
3
+ import { dirname, join } from "node:path"
4
+
5
+ import type { UserData } from "./adminAudit"
6
+
7
+ const TG_SAI_DIR_ENV = "TG_SAI_DIR"
8
+ const CONFIG_FILE = "profiles.json"
9
+
10
+ export interface TelegramCredentials {
11
+ apiId: number
12
+ apiHash: string
13
+ sessionString: string
14
+ }
15
+
16
+ export const TELEGRAM_PASSWORD_KEY = "TELEGRAM_PASSWORD" as const
17
+
18
+ export const TELEGRAM_CREDENTIAL_KEYS = [
19
+ "TELEGRAM_API_ID",
20
+ "TELEGRAM_API_HASH",
21
+ "TELEGRAM_SESSION_STRING",
22
+ ] as const
23
+
24
+ export type TelegramCredentialKey = (typeof TELEGRAM_CREDENTIAL_KEYS)[number]
25
+ export type TelegramProfileValueKey =
26
+ | TelegramCredentialKey
27
+ | typeof TELEGRAM_PASSWORD_KEY
28
+
29
+ export type TelegramCredentialValues = Partial<
30
+ Record<TelegramProfileValueKey, string>
31
+ >
32
+
33
+ export interface TgSaiProfile {
34
+ name: string
35
+ apiId: number | null
36
+ apiHash: string
37
+ sessionString: string
38
+ password: string
39
+ handle: string | null
40
+ userId: number | null
41
+ displayName: string
42
+ createdAt: string
43
+ updatedAt: string
44
+ }
45
+
46
+ export interface TgSaiConfig {
47
+ activeProfile: string | null
48
+ profiles: Record<string, TgSaiProfile>
49
+ }
50
+
51
+ export interface ProfileAddInput {
52
+ name: string
53
+ apiId: number | null
54
+ apiHash: string
55
+ sessionString: string
56
+ password: string
57
+ handle: string | null
58
+ userId: number | null
59
+ displayName: string
60
+ }
61
+
62
+ export interface ProfileStoreEnv {
63
+ [key: string]: string | undefined
64
+ }
65
+
66
+ export type PlatformKind = "macos" | "linux" | "windows" | "unknown"
67
+
68
+ export interface DiscoveryPath {
69
+ name: string
70
+ path: string
71
+ expandedPath: string
72
+ platform: PlatformKind
73
+ }
74
+
75
+ export interface DiscoveryCandidate {
76
+ name: string
77
+ path: string
78
+ objectPath: string
79
+ values: TelegramCredentialValues
80
+ credentials: TelegramCredentials | null
81
+ missingKeys: TelegramCredentialKey[]
82
+ }
83
+
84
+ export interface DiscoveryReport {
85
+ platform: PlatformKind
86
+ home: string
87
+ checkedPaths: DiscoveryPath[]
88
+ foundFiles: string[]
89
+ candidates: DiscoveryCandidate[]
90
+ }
91
+
92
+ export const TELEGRAM_PROFILE_VALUE_KEYS = [
93
+ ...TELEGRAM_CREDENTIAL_KEYS,
94
+ TELEGRAM_PASSWORD_KEY,
95
+ ] as const
96
+
97
+ export const MACOS_DISCOVERY_PATHS = [
98
+ // Claude Desktop stores MCP server definitions in Application Support on macOS.
99
+ {
100
+ name: "Claude Desktop",
101
+ path: "~/Library/Application Support/Claude/claude_desktop_config.json",
102
+ },
103
+ // Cursor stores workspace/global MCP definitions under the user's .cursor dir.
104
+ {
105
+ name: "Cursor",
106
+ path: "~/.cursor/mcp.json",
107
+ },
108
+ // Claude Code stores account/tooling state in the user's Claude config file.
109
+ {
110
+ name: "Claude Code",
111
+ path: "~/.claude.json",
112
+ },
113
+ ] as const
114
+
115
+ export const LINUX_DISCOVERY_PATHS = [
116
+ // Cursor uses the same user-level MCP config path on Linux.
117
+ {
118
+ name: "Cursor",
119
+ path: "~/.cursor/mcp.json",
120
+ },
121
+ // Claude Code keeps its user config in ~/.claude.json on Linux.
122
+ {
123
+ name: "Claude Code",
124
+ path: "~/.claude.json",
125
+ },
126
+ ] as const
127
+
128
+ export const WINDOWS_DISCOVERY_PATHS = [
129
+ // Cursor's Windows MCP config lives under the roaming application data tree.
130
+ {
131
+ name: "Cursor",
132
+ path: "~/AppData/Roaming/Cursor/User/mcp.json",
133
+ },
134
+ // Keep Claude Code's user-level config as a fallback when HOME is available.
135
+ {
136
+ name: "Claude Code",
137
+ path: "~/.claude.json",
138
+ },
139
+ ] as const
140
+
141
+ export const emptyConfig = (): TgSaiConfig => ({
142
+ activeProfile: null,
143
+ profiles: {},
144
+ })
145
+
146
+ export function homeDir(env: ProfileStoreEnv = process.env): string {
147
+ return env.HOME ?? homedir()
148
+ }
149
+
150
+ export function platformKind(platform = osPlatform()): PlatformKind {
151
+ switch (platform) {
152
+ case "darwin":
153
+ return "macos"
154
+ case "linux":
155
+ return "linux"
156
+ case "win32":
157
+ return "windows"
158
+ default:
159
+ return "unknown"
160
+ }
161
+ }
162
+
163
+ export function discoveryPathTemplates(platform: PlatformKind) {
164
+ switch (platform) {
165
+ case "macos":
166
+ return MACOS_DISCOVERY_PATHS
167
+ case "linux":
168
+ return LINUX_DISCOVERY_PATHS
169
+ case "windows":
170
+ return WINDOWS_DISCOVERY_PATHS
171
+ default:
172
+ return []
173
+ }
174
+ }
175
+
176
+ export function expandHome(path: string, env: ProfileStoreEnv = process.env) {
177
+ return path.startsWith("~/") ? join(homeDir(env), path.slice(2)) : path
178
+ }
179
+
180
+ export function tgSaiRepoDir(env: ProfileStoreEnv = process.env): string {
181
+ return env.TG_SAI_REPO_DIR ?? import.meta.dir
182
+ }
183
+
184
+ function tgSaiEnvDiscoveryPaths(
185
+ env: ProfileStoreEnv = process.env,
186
+ platform = platformKind(),
187
+ ): DiscoveryPath[] {
188
+ const repoEnv = join(tgSaiRepoDir(env), ".env")
189
+ const userEnv = join(configDir(env), ".env")
190
+ return [
191
+ {
192
+ name: "tg-sai repo .env",
193
+ path: repoEnv,
194
+ expandedPath: repoEnv,
195
+ platform,
196
+ },
197
+ {
198
+ name: "tg-sai user .env",
199
+ path: userEnv,
200
+ expandedPath: userEnv,
201
+ platform,
202
+ },
203
+ ]
204
+ }
205
+
206
+ export function discoveryPaths(
207
+ env: ProfileStoreEnv = process.env,
208
+ platform = platformKind(),
209
+ ): DiscoveryPath[] {
210
+ return [
211
+ ...tgSaiEnvDiscoveryPaths(env, platform),
212
+ ...discoveryPathTemplates(platform).map((candidate) => ({
213
+ ...candidate,
214
+ expandedPath: expandHome(candidate.path, env),
215
+ platform,
216
+ })),
217
+ ]
218
+ }
219
+
220
+ /**
221
+ * Returns the self-contained tg-sai tool directory.
222
+ *
223
+ * Environment variable `TG_SAI_DIR` overrides the default for tests and one-off
224
+ * runs.
225
+ */
226
+ export function configDir(env: ProfileStoreEnv = process.env): string {
227
+ return env[TG_SAI_DIR_ENV] ?? join(homeDir(env), ".tg-sai")
228
+ }
229
+
230
+ export function configPath(env: ProfileStoreEnv = process.env): string {
231
+ return join(configDir(env), CONFIG_FILE)
232
+ }
233
+
234
+ export async function readConfig(
235
+ env: ProfileStoreEnv = process.env,
236
+ ): Promise<TgSaiConfig> {
237
+ try {
238
+ const raw = await readFile(configPath(env), "utf8")
239
+ const parsed = JSON.parse(raw) as TgSaiConfig
240
+ for (const profile of Object.values(parsed.profiles)) {
241
+ profile.password = profile.password ?? ""
242
+ }
243
+ return parsed
244
+ } catch (error) {
245
+ if (
246
+ error instanceof Error &&
247
+ "code" in error &&
248
+ (error as NodeJS.ErrnoException).code === "ENOENT"
249
+ ) {
250
+ return emptyConfig()
251
+ }
252
+ throw error
253
+ }
254
+ }
255
+
256
+ export async function writeConfig(
257
+ config: TgSaiConfig,
258
+ env: ProfileStoreEnv = process.env,
259
+ ): Promise<void> {
260
+ const path = configPath(env)
261
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
262
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, {
263
+ mode: 0o600,
264
+ })
265
+ }
266
+
267
+ export async function clearConfig(
268
+ env: ProfileStoreEnv = process.env,
269
+ ): Promise<void> {
270
+ await rm(configPath(env), { force: true })
271
+ }
272
+
273
+ export function profileFromInput(
274
+ input: ProfileAddInput,
275
+ now = new Date(),
276
+ ): TgSaiProfile {
277
+ const timestamp = now.toISOString()
278
+ return {
279
+ ...input,
280
+ createdAt: timestamp,
281
+ updatedAt: timestamp,
282
+ }
283
+ }
284
+
285
+ export async function addProfile(
286
+ input: ProfileAddInput,
287
+ env: ProfileStoreEnv = process.env,
288
+ now = new Date(),
289
+ ): Promise<TgSaiConfig> {
290
+ const config = await readConfig(env)
291
+ const previous = config.profiles[input.name]
292
+ const timestamp = now.toISOString()
293
+ config.profiles[input.name] = {
294
+ ...previous,
295
+ ...input,
296
+ password: input.password || previous?.password || "",
297
+ createdAt: previous?.createdAt ?? timestamp,
298
+ updatedAt: timestamp,
299
+ }
300
+
301
+ const profileCount = Object.keys(config.profiles).length
302
+ if (config.activeProfile === null || profileCount === 1) {
303
+ config.activeProfile = input.name
304
+ }
305
+
306
+ await writeConfig(config, env)
307
+ return config
308
+ }
309
+
310
+ export async function removeProfile(
311
+ name: string,
312
+ env: ProfileStoreEnv = process.env,
313
+ ): Promise<TgSaiConfig> {
314
+ const config = await readConfig(env)
315
+ delete config.profiles[name]
316
+
317
+ if (config.activeProfile === name) {
318
+ const remaining = Object.keys(config.profiles)
319
+ config.activeProfile = remaining.length === 1 ? remaining[0]! : null
320
+ }
321
+
322
+ await writeConfig(config, env)
323
+ return config
324
+ }
325
+
326
+ export async function setActiveProfile(
327
+ name: string,
328
+ env: ProfileStoreEnv = process.env,
329
+ ): Promise<TgSaiConfig> {
330
+ const config = await readConfig(env)
331
+ if (!config.profiles[name]) {
332
+ throw new Error(`Unknown profile: ${name}`)
333
+ }
334
+ config.activeProfile = name
335
+ await writeConfig(config, env)
336
+ return config
337
+ }
338
+
339
+ export function getProfile(
340
+ config: TgSaiConfig,
341
+ name: string | undefined,
342
+ ): TgSaiProfile | null {
343
+ const profileName = name ?? config.activeProfile
344
+ return profileName ? (config.profiles[profileName] ?? null) : null
345
+ }
346
+
347
+ export function redactedProfile(profile: TgSaiProfile) {
348
+ return {
349
+ name: profile.name,
350
+ handle: profile.handle,
351
+ userId: profile.userId,
352
+ displayName: profile.displayName,
353
+ apiId: profile.apiId,
354
+ apiHash: profile.apiHash ? "[redacted]" : "",
355
+ sessionString: profile.sessionString ? "[redacted]" : "",
356
+ password: profile.password ? "[redacted]" : "",
357
+ createdAt: profile.createdAt,
358
+ updatedAt: profile.updatedAt,
359
+ }
360
+ }
361
+
362
+ export function credentialsFromProfileRecord(
363
+ profile: TgSaiProfile,
364
+ ): TelegramCredentials | null {
365
+ if (!profile.apiId || !profile.apiHash || !profile.sessionString) {
366
+ return null
367
+ }
368
+ return {
369
+ apiId: profile.apiId,
370
+ apiHash: profile.apiHash,
371
+ sessionString: profile.sessionString,
372
+ }
373
+ }
374
+
375
+ export function credentialsFromEnv(
376
+ env: ProfileStoreEnv = process.env,
377
+ ): TelegramCredentials | null {
378
+ const apiId = env.TELEGRAM_API_ID
379
+ const apiHash = env.TELEGRAM_API_HASH
380
+ const sessionString = env.TELEGRAM_SESSION_STRING
381
+ if (!apiId || !apiHash || !sessionString) {
382
+ return null
383
+ }
384
+ return {
385
+ apiId: Number(apiId),
386
+ apiHash,
387
+ sessionString,
388
+ }
389
+ }
390
+
391
+ function unquote(value: string): string {
392
+ return value.trim().replace(/^['"]|['"]$/g, "")
393
+ }
394
+
395
+ export function credentialsFromText(text: string): TelegramCredentials | null {
396
+ const values = telegramCredentialValuesFromLines(text)
397
+ const apiId = values.TELEGRAM_API_ID
398
+ const apiHash = values.TELEGRAM_API_HASH
399
+ const sessionString = values.TELEGRAM_SESSION_STRING
400
+ if (!apiId || !apiHash || !sessionString) {
401
+ return null
402
+ }
403
+
404
+ return {
405
+ apiId: Number(apiId),
406
+ apiHash,
407
+ sessionString,
408
+ }
409
+ }
410
+
411
+ export function parseCredentialValueAfterKey(afterKey: string): string | null {
412
+ const trimmed = afterKey.trimStart()
413
+ const withoutSeparator = trimmed.replace(/^["']?\s*[:=]\s*/, "").trimStart()
414
+ if (withoutSeparator.length === 0) {
415
+ return null
416
+ }
417
+
418
+ const quote = withoutSeparator[0]
419
+ if (quote === '"' || quote === "'") {
420
+ let value = ""
421
+ for (let index = 1; index < withoutSeparator.length; index += 1) {
422
+ const char = withoutSeparator[index]
423
+ if (char === quote && withoutSeparator[index - 1] !== "\\") {
424
+ return value
425
+ }
426
+ value += char
427
+ }
428
+ return value.length > 0 ? value : null
429
+ }
430
+
431
+ const unquoted = withoutSeparator.split(/[,\s#]/, 1)[0]?.trim()
432
+ return unquoted && unquoted.length > 0 ? unquoted : null
433
+ }
434
+
435
+ export function parseCredentialLine(line: string): TelegramCredentialValues {
436
+ for (const key of TELEGRAM_PROFILE_VALUE_KEYS) {
437
+ const index = line.indexOf(key)
438
+ if (index === -1) {
439
+ continue
440
+ }
441
+
442
+ const afterKey = line.slice(index + key.length)
443
+ const value = parseCredentialValueAfterKey(afterKey)
444
+ return value ? { [key]: value } : {}
445
+ }
446
+ return {}
447
+ }
448
+
449
+ export function telegramCredentialValuesFromLines(
450
+ text: string,
451
+ ): TelegramCredentialValues {
452
+ return text
453
+ .split(/\r?\n/)
454
+ .map(parseCredentialLine)
455
+ .reduce<TelegramCredentialValues>(
456
+ (acc, values) => ({ ...acc, ...values }),
457
+ {},
458
+ )
459
+ }
460
+
461
+ function credentialValuesFromObject(value: unknown): TelegramCredentialValues {
462
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
463
+ return {}
464
+ }
465
+
466
+ const record = value as Record<string, unknown>
467
+ const hasCandidateKey = TELEGRAM_PROFILE_VALUE_KEYS.some(
468
+ (key) => key in record,
469
+ )
470
+ if (!hasCandidateKey) {
471
+ return {}
472
+ }
473
+
474
+ return TELEGRAM_PROFILE_VALUE_KEYS.reduce<TelegramCredentialValues>(
475
+ (acc, key) => {
476
+ const valueForKey = record[key]
477
+ if (typeof valueForKey === "string" || typeof valueForKey === "number") {
478
+ acc[key] = String(valueForKey)
479
+ }
480
+ return acc
481
+ },
482
+ {},
483
+ )
484
+ }
485
+
486
+ function credentialsFromCredentialValues(
487
+ values: TelegramCredentialValues,
488
+ ): TelegramCredentials | null {
489
+ const apiId = values.TELEGRAM_API_ID
490
+ const apiHash = values.TELEGRAM_API_HASH
491
+ const sessionString = values.TELEGRAM_SESSION_STRING
492
+ if (!apiId || !apiHash || !sessionString) {
493
+ return null
494
+ }
495
+
496
+ return {
497
+ apiId: Number(apiId),
498
+ apiHash,
499
+ sessionString,
500
+ }
501
+ }
502
+
503
+ function missingCredentialKeys(
504
+ values: TelegramCredentialValues,
505
+ ): TelegramCredentialKey[] {
506
+ return TELEGRAM_CREDENTIAL_KEYS.filter((key) => !values[key])
507
+ }
508
+
509
+ export function findTelegramCredentialBlocks(
510
+ value: unknown,
511
+ objectPath = "$",
512
+ ): Array<{
513
+ objectPath: string
514
+ values: TelegramCredentialValues
515
+ credentials: TelegramCredentials | null
516
+ missingKeys: TelegramCredentialKey[]
517
+ }> {
518
+ const values = credentialValuesFromObject(value)
519
+ const hasValues = Object.keys(values).length > 0
520
+ const found = hasValues
521
+ ? [
522
+ {
523
+ objectPath,
524
+ values,
525
+ credentials: credentialsFromCredentialValues(values),
526
+ missingKeys: missingCredentialKeys(values),
527
+ },
528
+ ]
529
+ : []
530
+
531
+ if (typeof value !== "object" || value === null) {
532
+ return found
533
+ }
534
+
535
+ if (Array.isArray(value)) {
536
+ value.forEach((item, index) => {
537
+ found.push(
538
+ ...findTelegramCredentialBlocks(item, `${objectPath}[${index}]`),
539
+ )
540
+ })
541
+ return found
542
+ }
543
+
544
+ for (const [key, child] of Object.entries(value)) {
545
+ found.push(...findTelegramCredentialBlocks(child, `${objectPath}.${key}`))
546
+ }
547
+ return found
548
+ }
549
+
550
+ export async function discoveryReport(
551
+ env: ProfileStoreEnv = process.env,
552
+ platform = platformKind(),
553
+ ): Promise<DiscoveryReport> {
554
+ const checkedPaths = discoveryPaths(env, platform)
555
+ const foundFiles: string[] = []
556
+ const candidates: DiscoveryCandidate[] = []
557
+
558
+ for (const candidatePath of checkedPaths) {
559
+ try {
560
+ const text = await readFile(candidatePath.expandedPath, "utf8")
561
+ foundFiles.push(candidatePath.expandedPath)
562
+ let blocks: Array<{
563
+ objectPath: string
564
+ values: TelegramCredentialValues
565
+ credentials: TelegramCredentials | null
566
+ missingKeys: TelegramCredentialKey[]
567
+ }> = []
568
+ try {
569
+ const parsed = JSON.parse(text) as unknown
570
+ blocks = findTelegramCredentialBlocks(parsed)
571
+ } catch (error) {
572
+ if (!(error instanceof SyntaxError)) {
573
+ throw error
574
+ }
575
+ }
576
+ if (blocks.length === 0) {
577
+ const values = telegramCredentialValuesFromLines(text)
578
+ if (Object.keys(values).length > 0) {
579
+ blocks = [
580
+ {
581
+ objectPath: "$.lineScan",
582
+ values,
583
+ credentials: credentialsFromCredentialValues(values),
584
+ missingKeys: missingCredentialKeys(values),
585
+ },
586
+ ]
587
+ }
588
+ }
589
+ for (const block of blocks) {
590
+ candidates.push({
591
+ name: candidatePath.name,
592
+ path: candidatePath.expandedPath,
593
+ objectPath: block.objectPath,
594
+ values: block.values,
595
+ credentials: block.credentials,
596
+ missingKeys: block.missingKeys,
597
+ })
598
+ }
599
+ } catch (error) {
600
+ if (
601
+ error instanceof Error &&
602
+ "code" in error &&
603
+ (error as NodeJS.ErrnoException).code === "ENOENT"
604
+ ) {
605
+ continue
606
+ }
607
+ throw error
608
+ }
609
+ }
610
+ return {
611
+ platform,
612
+ home: homeDir(env),
613
+ checkedPaths,
614
+ foundFiles,
615
+ candidates,
616
+ }
617
+ }
618
+
619
+ export async function credentialsFromKnownFiles(
620
+ env: ProfileStoreEnv = process.env,
621
+ ): Promise<TelegramCredentials | null> {
622
+ if (env.TG_SAI_DISCOVERY_FILE) {
623
+ const text = await readFile(env.TG_SAI_DISCOVERY_FILE, "utf8")
624
+ const textCredentials = credentialsFromText(text)
625
+ if (textCredentials) {
626
+ return textCredentials
627
+ }
628
+ try {
629
+ const parsed = JSON.parse(text) as unknown
630
+ const complete = findTelegramCredentialBlocks(parsed).find(
631
+ (candidate) => candidate.credentials !== null,
632
+ )
633
+ return complete?.credentials ?? null
634
+ } catch (error) {
635
+ if (error instanceof SyntaxError) {
636
+ return null
637
+ }
638
+ throw error
639
+ }
640
+ }
641
+
642
+ const report = await discoveryReport(env)
643
+ const completeCandidates = report.candidates.filter(
644
+ (candidate) => candidate.credentials !== null,
645
+ )
646
+ if (completeCandidates.length === 1) {
647
+ return completeCandidates[0]!.credentials
648
+ }
649
+ if (completeCandidates.length > 1) {
650
+ throw new Error(
651
+ `Multiple Telegram credential sources found: ${completeCandidates
652
+ .map((candidate) => `${candidate.name} ${candidate.objectPath}`)
653
+ .join(", ")}`,
654
+ )
655
+ }
656
+ return null
657
+ }
658
+
659
+ export async function discoverTelegramCredentials(
660
+ env: ProfileStoreEnv = process.env,
661
+ ): Promise<TelegramCredentials | null> {
662
+ return credentialsFromEnv(env) ?? (await credentialsFromKnownFiles(env))
663
+ }
664
+
665
+ export async function credentialsFromProfile(
666
+ profileName: string | undefined,
667
+ env: ProfileStoreEnv = process.env,
668
+ ): Promise<TelegramCredentials | null> {
669
+ const config = await readConfig(env)
670
+ const profile = getProfile(config, profileName)
671
+ if (!profile) {
672
+ return null
673
+ }
674
+ return credentialsFromProfileRecord(profile)
675
+ }
676
+
677
+ export async function resolveTelegramCredentials(
678
+ profileName: string | undefined,
679
+ env: ProfileStoreEnv = process.env,
680
+ ): Promise<TelegramCredentials> {
681
+ const envCredentials = credentialsFromEnv(env)
682
+ if (envCredentials) {
683
+ return envCredentials
684
+ }
685
+
686
+ const config = await readConfig(env)
687
+ const selectedProfile = getProfile(config, profileName)
688
+ const profileCredentials = selectedProfile
689
+ ? credentialsFromProfileRecord(selectedProfile)
690
+ : null
691
+ if (profileCredentials) {
692
+ return profileCredentials
693
+ }
694
+ if (selectedProfile) {
695
+ throw new Error(
696
+ `Profile ${selectedProfile.name} is incomplete. Fill apiId, apiHash, and sessionString before running Telegram operations.`,
697
+ )
698
+ }
699
+
700
+ const knownFileCredentials = await credentialsFromKnownFiles(env)
701
+ if (knownFileCredentials) {
702
+ return knownFileCredentials
703
+ }
704
+
705
+ throw new Error(
706
+ "Missing Telegram credentials. Set TELEGRAM_API_ID, TELEGRAM_API_HASH, and TELEGRAM_SESSION_STRING or run `tg-sai profile add`.",
707
+ )
708
+ }
709
+
710
+ export function profileInputFromUser(
711
+ name: string,
712
+ credentials: TelegramCredentials,
713
+ user: UserData,
714
+ password = "",
715
+ ): ProfileAddInput {
716
+ return {
717
+ name,
718
+ apiId: credentials.apiId,
719
+ apiHash: credentials.apiHash,
720
+ sessionString: credentials.sessionString,
721
+ password,
722
+ handle: user.username,
723
+ userId: user.id,
724
+ displayName: user.displayName,
725
+ }
726
+ }