@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/README.md +21 -0
- package/RUNBOOK.md +343 -0
- package/actionExecutor.ts +219 -0
- package/adminAudit.ts +147 -0
- package/cfg.ts +29 -0
- package/health.ts +479 -0
- package/main.ts +1290 -0
- package/mtcuteAdapter.ts +268 -0
- package/ops.ts +105 -0
- package/package.json +41 -0
- package/profiles.ts +726 -0
- package/pyproject.toml +20 -0
- package/session_string_generator.py +267 -0
- package/uv.lock +227 -0
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
|
+
}
|