@uniquedivine/tg-sai 0.0.4 → 0.0.5
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 +24 -0
- package/RUNBOOK.md +137 -16
- package/actionExecutor.ts +55 -55
- package/adminAudit.ts +60 -60
- package/captureProvisioningGolden.ts +98 -0
- package/cfg.ts +12 -1
- package/custodyWorker.ts +259 -0
- package/custodyWorkerCli.ts +52 -0
- package/health.ts +31 -50
- package/main.ts +442 -100
- package/mtcuteAdapter.ts +14 -6
- package/ops.ts +12 -0
- package/package.json +5 -4
- package/profiles.ts +134 -27
- package/provisioning.ts +674 -0
- package/provisioningCli.ts +140 -0
- package/provisioningCloud.ts +214 -0
- package/provisioningGoldens.ts +70 -0
- package/provisioningRuntime.ts +380 -0
- package/runtimeAssets.d.ts +4 -0
- package/runtimeHealth.ts +125 -0
- package/runtimeHealthCli.ts +46 -0
- package/session_string_generator.py +36 -24
- package/telegramRuntime.ts +25 -0
package/main.ts
CHANGED
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
import { createRequire } from "node:module"
|
|
12
12
|
import { tmpdir } from "node:os"
|
|
13
13
|
import { join } from "node:path"
|
|
14
|
+
import { stdin as input, stdout as output } from "node:process"
|
|
15
|
+
import { createInterface } from "node:readline/promises"
|
|
14
16
|
import { Command } from "commander"
|
|
15
|
-
|
|
17
|
+
import { addProvisionCommands } from "./provisioningCli"
|
|
18
|
+
import { addCustodyWorkerCommands } from "./custodyWorkerCli"
|
|
16
19
|
import type { BatchLogger } from "./actionExecutor"
|
|
17
20
|
import { withFloodWaitRetry } from "./actionExecutor"
|
|
18
21
|
import type {
|
|
@@ -23,6 +26,7 @@ import type {
|
|
|
23
26
|
import { planAdminBootstrap } from "./adminAudit"
|
|
24
27
|
import { saiBot, USERS } from "./cfg"
|
|
25
28
|
import { runHealthCheck } from "./health"
|
|
29
|
+
import { addRuntimeHealthCommands } from "./runtimeHealthCli"
|
|
26
30
|
import {
|
|
27
31
|
addTargetToChat,
|
|
28
32
|
createMtcuteClient,
|
|
@@ -32,14 +36,20 @@ import {
|
|
|
32
36
|
promoteTargetAdmin,
|
|
33
37
|
transferTargetOwnership,
|
|
34
38
|
} from "./mtcuteAdapter"
|
|
35
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
formatSaiChatJsonLine,
|
|
41
|
+
parseChatInputFile,
|
|
42
|
+
resolveBootstrapTarget,
|
|
43
|
+
} from "./ops"
|
|
36
44
|
import type { ChatInput } from "./ops"
|
|
37
45
|
import {
|
|
38
46
|
addProfile,
|
|
39
47
|
configDir,
|
|
40
48
|
discoveryReport,
|
|
49
|
+
findProfileIndex,
|
|
41
50
|
getProfile,
|
|
42
51
|
profileInputFromUser,
|
|
52
|
+
profileLabel,
|
|
43
53
|
readConfig,
|
|
44
54
|
redactedProfile,
|
|
45
55
|
removeProfile,
|
|
@@ -106,8 +116,11 @@ interface SaiBootstrapOptions {
|
|
|
106
116
|
run?: boolean
|
|
107
117
|
/** Attempt ownership transfer when the Telegram password is available. */
|
|
108
118
|
transferOwner?: boolean
|
|
119
|
+
/** Owner target: user ID or username. Defaults to the Sai custody account. */
|
|
120
|
+
targetUser?: number | string
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
|
|
111
124
|
interface TelegramEnv {
|
|
112
125
|
apiId: number
|
|
113
126
|
apiHash: string
|
|
@@ -116,7 +129,7 @@ interface TelegramEnv {
|
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
interface ProfileAddOptions {
|
|
119
|
-
name
|
|
132
|
+
name?: string
|
|
120
133
|
apiId?: string
|
|
121
134
|
apiHash?: string
|
|
122
135
|
sessionString?: string
|
|
@@ -128,6 +141,7 @@ interface ProfileAddOptions {
|
|
|
128
141
|
login?: boolean
|
|
129
142
|
qr?: boolean
|
|
130
143
|
phone?: boolean
|
|
144
|
+
interactive?: boolean
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
const saiBotAdminRights = {
|
|
@@ -285,12 +299,15 @@ async function getTelegramEnv(profileName?: string): Promise<TelegramEnv> {
|
|
|
285
299
|
}
|
|
286
300
|
}
|
|
287
301
|
|
|
288
|
-
async function selectedProfileName(
|
|
302
|
+
async function selectedProfileName(
|
|
303
|
+
profileName?: string,
|
|
304
|
+
): Promise<string | null> {
|
|
289
305
|
if (profileName) {
|
|
290
306
|
return profileName
|
|
291
307
|
}
|
|
292
308
|
const config = await readConfig()
|
|
293
|
-
|
|
309
|
+
const profile = config.profiles[0]
|
|
310
|
+
return profile ? profileLabel(profile, 0) : null
|
|
294
311
|
}
|
|
295
312
|
|
|
296
313
|
function parsePeer(value: string): number | string {
|
|
@@ -774,6 +791,7 @@ async function runSaiBootstrap(options: SaiBootstrapOptions): Promise<void> {
|
|
|
774
791
|
const actor = normalizeUser(await client.getMe())
|
|
775
792
|
const actorUsername = actor.username ?? ""
|
|
776
793
|
const profileName = await selectedProfileName(options.profile)
|
|
794
|
+
const ownerTarget = resolveBootstrapTarget(options.targetUser)
|
|
777
795
|
const ownershipTransferEnabled =
|
|
778
796
|
options.transferOwner !== false && env.password !== null
|
|
779
797
|
console.log(`wrote_run_log: ${logPath}`)
|
|
@@ -787,7 +805,7 @@ async function runSaiBootstrap(options: SaiBootstrapOptions): Promise<void> {
|
|
|
787
805
|
transferOwner: ownershipTransferEnabled,
|
|
788
806
|
passwordAvailable: env.password !== null,
|
|
789
807
|
delayMs,
|
|
790
|
-
targetUser:
|
|
808
|
+
targetUser: ownerTarget,
|
|
791
809
|
targetBot: saiBot.id,
|
|
792
810
|
})
|
|
793
811
|
|
|
@@ -816,8 +834,8 @@ async function runSaiBootstrap(options: SaiBootstrapOptions): Promise<void> {
|
|
|
816
834
|
client,
|
|
817
835
|
chat,
|
|
818
836
|
actorUsername,
|
|
819
|
-
targetUserId:
|
|
820
|
-
targetPeer:
|
|
837
|
+
targetUserId: ownerTarget,
|
|
838
|
+
targetPeer: ownerTarget,
|
|
821
839
|
rights: ownerTargetAdminRights,
|
|
822
840
|
actionPrefix: "owner_target",
|
|
823
841
|
run: options.run === true,
|
|
@@ -827,7 +845,7 @@ async function runSaiBootstrap(options: SaiBootstrapOptions): Promise<void> {
|
|
|
827
845
|
env,
|
|
828
846
|
chat,
|
|
829
847
|
actorUsername,
|
|
830
|
-
targetUser:
|
|
848
|
+
targetUser: ownerTarget,
|
|
831
849
|
enabled: ownershipTransferEnabled,
|
|
832
850
|
run: options.run === true,
|
|
833
851
|
})
|
|
@@ -866,19 +884,6 @@ async function runSaiBootstrap(options: SaiBootstrapOptions): Promise<void> {
|
|
|
866
884
|
})
|
|
867
885
|
}
|
|
868
886
|
|
|
869
|
-
function getCredentialsFromProfileAddOptions(
|
|
870
|
-
options: ProfileAddOptions,
|
|
871
|
-
): TelegramCredentials | null {
|
|
872
|
-
if (options.apiId && options.apiHash && options.sessionString) {
|
|
873
|
-
return {
|
|
874
|
-
apiId: Number(options.apiId),
|
|
875
|
-
apiHash: options.apiHash,
|
|
876
|
-
sessionString: options.sessionString,
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
return null
|
|
880
|
-
}
|
|
881
|
-
|
|
882
887
|
function credentialValuesFromProfileAddOptions(
|
|
883
888
|
options: ProfileAddOptions,
|
|
884
889
|
): TelegramCredentialValues {
|
|
@@ -890,6 +895,17 @@ function credentialValuesFromProfileAddOptions(
|
|
|
890
895
|
}
|
|
891
896
|
}
|
|
892
897
|
|
|
898
|
+
function credentialValuesFromEnv(
|
|
899
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
900
|
+
): TelegramCredentialValues {
|
|
901
|
+
return {
|
|
902
|
+
TELEGRAM_API_ID: env.TELEGRAM_API_ID,
|
|
903
|
+
TELEGRAM_API_HASH: env.TELEGRAM_API_HASH,
|
|
904
|
+
TELEGRAM_SESSION_STRING: env.TELEGRAM_SESSION_STRING,
|
|
905
|
+
TELEGRAM_PASSWORD: env.TELEGRAM_PASSWORD,
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
893
909
|
function profileFieldsFromCredentialValues(values: TelegramCredentialValues) {
|
|
894
910
|
return {
|
|
895
911
|
apiId: values.TELEGRAM_API_ID ? Number(values.TELEGRAM_API_ID) : null,
|
|
@@ -899,6 +915,10 @@ function profileFieldsFromCredentialValues(values: TelegramCredentialValues) {
|
|
|
899
915
|
}
|
|
900
916
|
}
|
|
901
917
|
|
|
918
|
+
type ProfileCredentialFields = ReturnType<
|
|
919
|
+
typeof profileFieldsFromCredentialValues
|
|
920
|
+
>
|
|
921
|
+
|
|
902
922
|
function mergeCredentialValues(
|
|
903
923
|
...valuesList: Array<TelegramCredentialValues | null>
|
|
904
924
|
): TelegramCredentialValues {
|
|
@@ -916,10 +936,112 @@ function mergeCredentialValues(
|
|
|
916
936
|
return merged
|
|
917
937
|
}
|
|
918
938
|
|
|
939
|
+
function redactedCredentialValue(key: string, value: string): string {
|
|
940
|
+
if (
|
|
941
|
+
key === "TELEGRAM_API_HASH" ||
|
|
942
|
+
key === "TELEGRAM_SESSION_STRING" ||
|
|
943
|
+
key === "TELEGRAM_PASSWORD"
|
|
944
|
+
) {
|
|
945
|
+
return value ? "[redacted]" : ""
|
|
946
|
+
}
|
|
947
|
+
return value
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function sourceLabel(candidate: DiscoveryReport["candidates"][number]): string {
|
|
951
|
+
return `${candidate.name} ${candidate.path} ${candidate.objectPath}`
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function mergeDiscoveredValues(
|
|
955
|
+
candidates: DiscoveryReport["candidates"],
|
|
956
|
+
): TelegramCredentialValues {
|
|
957
|
+
const merged: TelegramCredentialValues = {}
|
|
958
|
+
const origins: Partial<
|
|
959
|
+
Record<
|
|
960
|
+
keyof TelegramCredentialValues,
|
|
961
|
+
DiscoveryReport["candidates"][number]
|
|
962
|
+
>
|
|
963
|
+
> = {}
|
|
964
|
+
|
|
965
|
+
for (const candidate of candidates) {
|
|
966
|
+
for (const [key, value] of Object.entries(candidate.values)) {
|
|
967
|
+
if (value === undefined || value === "") {
|
|
968
|
+
continue
|
|
969
|
+
}
|
|
970
|
+
const typedKey = key as keyof TelegramCredentialValues
|
|
971
|
+
const previous = merged[typedKey]
|
|
972
|
+
if (previous !== undefined && previous !== value) {
|
|
973
|
+
const previousOrigin = origins[typedKey]!
|
|
974
|
+
throw new Error(
|
|
975
|
+
[
|
|
976
|
+
`Conflicting Telegram credential values for ${key}.`,
|
|
977
|
+
`- ${sourceLabel(previousOrigin)}: ${redactedCredentialValue(
|
|
978
|
+
key,
|
|
979
|
+
previous,
|
|
980
|
+
)}`,
|
|
981
|
+
`- ${sourceLabel(candidate)}: ${redactedCredentialValue(
|
|
982
|
+
key,
|
|
983
|
+
value,
|
|
984
|
+
)}`,
|
|
985
|
+
"Run `tg-sai profile find` and choose one source with `--src <number>`.",
|
|
986
|
+
].join("\n"),
|
|
987
|
+
)
|
|
988
|
+
}
|
|
989
|
+
merged[typedKey] = value
|
|
990
|
+
origins[typedKey] = candidate
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return merged
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function assertNoExplicitSourceConflicts(params: {
|
|
998
|
+
sourceValues: TelegramCredentialValues
|
|
999
|
+
higherPrecedenceValues: TelegramCredentialValues
|
|
1000
|
+
sourceDescription: string
|
|
1001
|
+
}): void {
|
|
1002
|
+
for (const [key, value] of Object.entries(params.sourceValues)) {
|
|
1003
|
+
if (value === undefined || value === "") {
|
|
1004
|
+
continue
|
|
1005
|
+
}
|
|
1006
|
+
const higherValue =
|
|
1007
|
+
params.higherPrecedenceValues[key as keyof TelegramCredentialValues]
|
|
1008
|
+
if (
|
|
1009
|
+
higherValue !== undefined &&
|
|
1010
|
+
higherValue !== "" &&
|
|
1011
|
+
higherValue !== value
|
|
1012
|
+
) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
[
|
|
1015
|
+
`Conflicting Telegram credential values for ${key}.`,
|
|
1016
|
+
`- ${params.sourceDescription}: ${redactedCredentialValue(
|
|
1017
|
+
key,
|
|
1018
|
+
value,
|
|
1019
|
+
)}`,
|
|
1020
|
+
`- CLI/env: ${redactedCredentialValue(key, higherValue)}`,
|
|
1021
|
+
"Remove the conflicting flag/env var or choose a different `--src`.",
|
|
1022
|
+
].join("\n"),
|
|
1023
|
+
)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
919
1028
|
function validateLoginOptions(options: ProfileAddOptions): void {
|
|
920
1029
|
if (options.qr && options.phone) {
|
|
921
1030
|
throw new Error("--qr and --phone are mutually exclusive")
|
|
922
1031
|
}
|
|
1032
|
+
if ((options.login || options.qr || options.phone) && options.sessionString) {
|
|
1033
|
+
throw new Error("Fresh login cannot be combined with --session-string")
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** A new login can reuse app credentials, never another account's session or 2FA. */
|
|
1038
|
+
function profileLoginValues(
|
|
1039
|
+
values: TelegramCredentialValues | null,
|
|
1040
|
+
options: ProfileAddOptions,
|
|
1041
|
+
): TelegramCredentialValues {
|
|
1042
|
+
if (!(options.login || options.qr || options.phone)) return values ?? {}
|
|
1043
|
+
const { TELEGRAM_SESSION_STRING: _session, TELEGRAM_PASSWORD: _password, ...app } = values ?? {}
|
|
1044
|
+
return app
|
|
923
1045
|
}
|
|
924
1046
|
|
|
925
1047
|
async function generateSessionString(params: {
|
|
@@ -965,49 +1087,139 @@ async function generateSessionString(params: {
|
|
|
965
1087
|
}
|
|
966
1088
|
}
|
|
967
1089
|
|
|
968
|
-
async function
|
|
1090
|
+
async function discoveredValuesForProfileAdd(
|
|
969
1091
|
options: ProfileAddOptions,
|
|
970
|
-
): Promise<
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
if (
|
|
976
|
-
|
|
1092
|
+
): Promise<{
|
|
1093
|
+
values: TelegramCredentialValues | null
|
|
1094
|
+
sourceDescription: string | null
|
|
1095
|
+
explicit: boolean
|
|
1096
|
+
}> {
|
|
1097
|
+
if (options.src) {
|
|
1098
|
+
const index = Number(options.src)
|
|
1099
|
+
if (!Number.isInteger(index) || index < 1) {
|
|
1100
|
+
throw new Error("--src must be a 1-based source number")
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const report = await discoveryReport()
|
|
1104
|
+
const candidate = report.candidates[index - 1]
|
|
1105
|
+
if (!candidate) {
|
|
1106
|
+
throw new Error(`Unknown discovery source: ${options.src}`)
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
values: profileLoginValues(candidate.values, options),
|
|
1110
|
+
sourceDescription: sourceLabel(candidate),
|
|
1111
|
+
explicit: true,
|
|
1112
|
+
}
|
|
977
1113
|
}
|
|
978
1114
|
|
|
979
1115
|
const report = await discoveryReport()
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1116
|
+
return {
|
|
1117
|
+
values: mergeDiscoveredValues(report.candidates.map((candidate) => ({
|
|
1118
|
+
...candidate, values: profileLoginValues(candidate.values, options),
|
|
1119
|
+
}))),
|
|
1120
|
+
sourceDescription: null,
|
|
1121
|
+
explicit: false,
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function promptForMissingProfileFields(
|
|
1126
|
+
profileFields: ProfileCredentialFields,
|
|
1127
|
+
options: ProfileAddOptions,
|
|
1128
|
+
): Promise<void> {
|
|
1129
|
+
if (!options.interactive) {
|
|
1130
|
+
return
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const rl = createInterface({ input, output })
|
|
1134
|
+
try {
|
|
1135
|
+
if (!profileFields.apiId) {
|
|
1136
|
+
const value = (await rl.question("Telegram API ID: ")).trim()
|
|
1137
|
+
if (value) {
|
|
1138
|
+
profileFields.apiId = Number(value)
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (!profileFields.apiHash) {
|
|
1142
|
+
const value = (await rl.question("Telegram API hash: ")).trim()
|
|
1143
|
+
if (value) {
|
|
1144
|
+
profileFields.apiHash = value
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (
|
|
1148
|
+
!profileFields.sessionString &&
|
|
1149
|
+
!options.qr &&
|
|
1150
|
+
!options.phone &&
|
|
1151
|
+
!options.login
|
|
1152
|
+
) {
|
|
1153
|
+
const value = (
|
|
1154
|
+
await rl.question("Generate a Telegram session now? [Y=QR/n/phone]: ")
|
|
1155
|
+
)
|
|
1156
|
+
.trim()
|
|
1157
|
+
.toLowerCase()
|
|
1158
|
+
if (value === "phone" || value === "p") {
|
|
1159
|
+
options.phone = true
|
|
1160
|
+
} else if (value === "" || value === "y" || value === "yes") {
|
|
1161
|
+
options.qr = true
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
} finally {
|
|
1165
|
+
rl.close()
|
|
983
1166
|
}
|
|
984
|
-
return candidate.values
|
|
985
1167
|
}
|
|
986
1168
|
|
|
987
1169
|
async function inputFromProfileAddOptions(
|
|
988
1170
|
options: ProfileAddOptions,
|
|
989
1171
|
): Promise<ProfileAddInput> {
|
|
990
1172
|
validateLoginOptions(options)
|
|
991
|
-
const
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
1173
|
+
const saved = getProfile(await readConfig(), options.name)
|
|
1174
|
+
const enrollsAccount = Boolean(
|
|
1175
|
+
options.login || options.qr || options.phone || options.src || options.sessionString,
|
|
1176
|
+
)
|
|
1177
|
+
if (saved && !enrollsAccount) {
|
|
1178
|
+
// Resolve the saved target before discovery. A password/app metadata edit
|
|
1179
|
+
// must never authenticate another discovered account and overwrite this one.
|
|
1180
|
+
if (
|
|
1181
|
+
(options.userId !== undefined && Number(options.userId) !== saved.userId) ||
|
|
1182
|
+
(options.handle !== undefined && options.handle !== saved.handle)
|
|
1183
|
+
) {
|
|
1184
|
+
throw new Error("Changing a saved account identity requires an explicit fresh login or session import")
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
name: saved.name,
|
|
1188
|
+
apiId: options.apiId === undefined ? saved.apiId : Number(options.apiId),
|
|
1189
|
+
apiHash: options.apiHash ?? saved.apiHash,
|
|
1190
|
+
sessionString: saved.sessionString,
|
|
1191
|
+
password: options.password ?? saved.password,
|
|
1192
|
+
handle: saved.handle,
|
|
1193
|
+
userId: saved.userId,
|
|
1194
|
+
displayName: options.displayName ?? saved.displayName,
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (!saved && options.password !== undefined && !enrollsAccount) {
|
|
1198
|
+
throw new Error(`Unknown configured profile: ${options.name ?? "active profile"}. Use tg-sai profile list and select it with --name; --src selects credential discovery sources.`)
|
|
1199
|
+
}
|
|
995
1200
|
const optionValues = credentialValuesFromProfileAddOptions(options)
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1201
|
+
const envValues = profileLoginValues(credentialValuesFromEnv(), options)
|
|
1202
|
+
const discovery = await discoveredValuesForProfileAdd(options)
|
|
1203
|
+
const higherPrecedenceValues = mergeCredentialValues(envValues, optionValues)
|
|
1204
|
+
if (discovery.explicit && discovery.values && discovery.sourceDescription) {
|
|
1205
|
+
assertNoExplicitSourceConflicts({
|
|
1206
|
+
sourceValues: discovery.values,
|
|
1207
|
+
higherPrecedenceValues,
|
|
1208
|
+
sourceDescription: discovery.sourceDescription,
|
|
1209
|
+
})
|
|
1210
|
+
}
|
|
1211
|
+
const profileFields = profileFieldsFromCredentialValues(
|
|
1212
|
+
mergeCredentialValues(discovery.values, envValues, optionValues),
|
|
1213
|
+
)
|
|
1214
|
+
await promptForMissingProfileFields(profileFields, options)
|
|
1006
1215
|
|
|
1007
|
-
if (
|
|
1216
|
+
if (
|
|
1217
|
+
(options.login || options.qr || options.phone) &&
|
|
1218
|
+
!profileFields.sessionString
|
|
1219
|
+
) {
|
|
1008
1220
|
if (!profileFields.apiId || !profileFields.apiHash) {
|
|
1009
1221
|
throw new Error(
|
|
1010
|
-
"
|
|
1222
|
+
"Session generation requires apiId and apiHash from flags, env, or discovery",
|
|
1011
1223
|
)
|
|
1012
1224
|
}
|
|
1013
1225
|
profileFields.sessionString = await generateSessionString({
|
|
@@ -1072,33 +1284,117 @@ async function inputFromProfileAddOptions(
|
|
|
1072
1284
|
|
|
1073
1285
|
async function runProfileList(): Promise<void> {
|
|
1074
1286
|
const config = await readConfig()
|
|
1075
|
-
|
|
1076
|
-
if (profiles.length === 0) {
|
|
1287
|
+
if (config.profiles.length === 0) {
|
|
1077
1288
|
console.log("No profiles configured.")
|
|
1078
1289
|
return
|
|
1079
1290
|
}
|
|
1080
|
-
for (const profile of profiles) {
|
|
1081
|
-
const active =
|
|
1291
|
+
for (const [index, profile] of config.profiles.entries()) {
|
|
1292
|
+
const active = index === 0 ? "*" : " "
|
|
1082
1293
|
console.log(
|
|
1083
|
-
`${active} ${
|
|
1294
|
+
`${active} ${index}\t${profileLabel(profile, index)}\t${profile.userId}\t${profile.displayName}`,
|
|
1084
1295
|
)
|
|
1085
1296
|
}
|
|
1086
1297
|
}
|
|
1087
1298
|
|
|
1088
|
-
async function runProfileShow(
|
|
1299
|
+
async function runProfileShow(selector = "0"): Promise<void> {
|
|
1089
1300
|
const config = await readConfig()
|
|
1090
|
-
const profile = getProfile(config,
|
|
1301
|
+
const profile = getProfile(config, selector)
|
|
1091
1302
|
if (!profile) {
|
|
1092
|
-
throw new Error(`Unknown profile: ${
|
|
1303
|
+
throw new Error(`Unknown profile: ${selector}`)
|
|
1093
1304
|
}
|
|
1094
1305
|
console.log(JSON.stringify(redactedProfile(profile), null, 2))
|
|
1095
1306
|
}
|
|
1096
1307
|
|
|
1308
|
+
async function printConfiguredProfiles(): Promise<void> {
|
|
1309
|
+
console.log("\nConfigured local profiles (* marks the active/default profile):")
|
|
1310
|
+
await runProfileList()
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function printProfileAddHelp(command: Command): Promise<void> {
|
|
1314
|
+
command.outputHelp()
|
|
1315
|
+
console.log(
|
|
1316
|
+
"\nNo profile is saved and no Telegram login starts without explicit options.",
|
|
1317
|
+
)
|
|
1318
|
+
console.log(
|
|
1319
|
+
"Use a new --name to add a separate account. Without --name, explicit options update the active profile.",
|
|
1320
|
+
)
|
|
1321
|
+
console.log("\nFresh QR login for a separate custody account:")
|
|
1322
|
+
console.log(
|
|
1323
|
+
" tg-sai profile add --name sai-custody --login --qr --interactive",
|
|
1324
|
+
)
|
|
1325
|
+
console.log(
|
|
1326
|
+
"Scan while signed into the intended Telegram account, then inspect it with:",
|
|
1327
|
+
)
|
|
1328
|
+
console.log(" tg-sai profile show sai-custody")
|
|
1329
|
+
await printConfiguredProfiles()
|
|
1330
|
+
console.log("\nInspect an existing profile with tg-sai profile show <selector>.")
|
|
1331
|
+
console.log("Update a saved password with --name <saved-name-or-handle-or-index> --password '<password>'.")
|
|
1332
|
+
console.log("--src selects a numbered credential source from profile find, not a saved profile index.")
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async function printProfileRemoveHelp(command: Command): Promise<void> {
|
|
1336
|
+
command.outputHelp()
|
|
1337
|
+
console.log(
|
|
1338
|
+
"\nRemoves a saved local profile, not the Telegram account or its chat memberships.",
|
|
1339
|
+
)
|
|
1340
|
+
console.log("Removing a profile does not revoke its Telegram sessions.")
|
|
1341
|
+
console.log("No profile is removed without an explicit selector.")
|
|
1342
|
+
const config = await readConfig()
|
|
1343
|
+
if (config.profiles.length === 0) {
|
|
1344
|
+
console.log("\nNo saved profiles are available to remove.")
|
|
1345
|
+
console.log(
|
|
1346
|
+
"Add an account with: tg-sai profile add --login --qr --interactive",
|
|
1347
|
+
)
|
|
1348
|
+
return
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
console.log("\nRunnable examples for saved profiles:")
|
|
1352
|
+
let examples = 0
|
|
1353
|
+
for (const [index, saved] of config.profiles.entries()) {
|
|
1354
|
+
// Handles and names can overlap. Emit only selectors that resolve to this row.
|
|
1355
|
+
const selector = [
|
|
1356
|
+
saved.handle,
|
|
1357
|
+
saved.name,
|
|
1358
|
+
saved.userId?.toString(),
|
|
1359
|
+
String(index),
|
|
1360
|
+
].find(
|
|
1361
|
+
(candidate) =>
|
|
1362
|
+
candidate != null && findProfileIndex(config, candidate) === index,
|
|
1363
|
+
)
|
|
1364
|
+
if (selector == null) continue
|
|
1365
|
+
const argument = /^[A-Za-z0-9_@./:-]+$/.test(selector)
|
|
1366
|
+
? selector
|
|
1367
|
+
: `'${selector.replaceAll("'", "'\\''")}'`
|
|
1368
|
+
const label = JSON.stringify(profileLabel(saved, index))
|
|
1369
|
+
console.log(
|
|
1370
|
+
` tg-sai profile remove -- ${argument} # ${label}${index === 0 ? " [active]" : ""}`,
|
|
1371
|
+
)
|
|
1372
|
+
examples++
|
|
1373
|
+
}
|
|
1374
|
+
if (examples < config.profiles.length) {
|
|
1375
|
+
console.log(
|
|
1376
|
+
"Some profiles have ambiguous selectors; inspect tg-sai profile list before removing them.",
|
|
1377
|
+
)
|
|
1378
|
+
}
|
|
1379
|
+
console.log(
|
|
1380
|
+
"\nExamples use the current profile configuration. Run tg-sai profile list to inspect it.",
|
|
1381
|
+
)
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1097
1384
|
async function runProfileAdd(options: ProfileAddOptions): Promise<void> {
|
|
1385
|
+
const previous = getProfile(await readConfig(), options.name)
|
|
1098
1386
|
const input = await inputFromProfileAddOptions(options)
|
|
1387
|
+
if (previous?.userId != null && input.userId !== previous.userId) {
|
|
1388
|
+
throw new Error("Login or import does not match the saved account. Use a new --name for another account, or explicitly remove the old profile before replacing it.")
|
|
1389
|
+
}
|
|
1390
|
+
if (previous) input.name = previous.name
|
|
1099
1391
|
const config = await addProfile(input)
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1392
|
+
const profile = getProfile(config, input.name)
|
|
1393
|
+
const index = profile ? config.profiles.indexOf(profile) : 0
|
|
1394
|
+
const savedProfile = profile ?? config.profiles[0]!
|
|
1395
|
+
const active = index === 0 ? "default" : "saved"
|
|
1396
|
+
console.log(`profile_${active}: ${profileLabel(savedProfile, index)}`)
|
|
1397
|
+
console.log(`profile_index: ${index}`)
|
|
1102
1398
|
console.log(`user_id: ${input.userId}`)
|
|
1103
1399
|
console.log(`handle: ${input.handle ?? ""}`)
|
|
1104
1400
|
console.log(`telegram_password: ${input.password ? "stored" : "missing"}`)
|
|
@@ -1134,7 +1430,23 @@ function printDiscoveryReport(report: DiscoveryReport): void {
|
|
|
1134
1430
|
)
|
|
1135
1431
|
}
|
|
1136
1432
|
console.log("next:")
|
|
1137
|
-
|
|
1433
|
+
try {
|
|
1434
|
+
const values = mergeDiscoveredValues(report.candidates)
|
|
1435
|
+
if (Object.keys(values).length === 0) {
|
|
1436
|
+
console.log(" tg-sai profile add")
|
|
1437
|
+
return
|
|
1438
|
+
}
|
|
1439
|
+
console.log(" tg-sai profile add")
|
|
1440
|
+
if (!values.TELEGRAM_SESSION_STRING) {
|
|
1441
|
+
console.log(" tg-sai profile add --qr")
|
|
1442
|
+
console.log(" tg-sai profile add --phone")
|
|
1443
|
+
}
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
console.log(
|
|
1446
|
+
` conflict: ${error instanceof Error ? error.message : String(error)}`,
|
|
1447
|
+
)
|
|
1448
|
+
console.log(" tg-sai profile add --src <number>")
|
|
1449
|
+
}
|
|
1138
1450
|
}
|
|
1139
1451
|
|
|
1140
1452
|
export function createProgram(): Command {
|
|
@@ -1150,22 +1462,13 @@ export function createProgram(): Command {
|
|
|
1150
1462
|
program.help({ error: false })
|
|
1151
1463
|
})
|
|
1152
1464
|
|
|
1153
|
-
program
|
|
1154
|
-
.command("health")
|
|
1155
|
-
.description("Read-only preflight for the local tg-sai setup")
|
|
1156
|
-
.action(async () => {
|
|
1157
|
-
await runHealth()
|
|
1158
|
-
})
|
|
1465
|
+
addRuntimeHealthCommands(program, runHealth)
|
|
1159
1466
|
|
|
1160
1467
|
program
|
|
1161
1468
|
.command("audit")
|
|
1162
1469
|
.description("Read-only audit for one chat and target user")
|
|
1163
1470
|
.option("--profile <name>", "Profile name to use for Telegram credentials")
|
|
1164
|
-
.requiredOption(
|
|
1165
|
-
"--chat <id>",
|
|
1166
|
-
"Telegram chat ID or username",
|
|
1167
|
-
parsePeer,
|
|
1168
|
-
)
|
|
1471
|
+
.requiredOption("--chat <id>", "Telegram chat ID or username", parsePeer)
|
|
1169
1472
|
.option(
|
|
1170
1473
|
"--target-user <id>",
|
|
1171
1474
|
"Target user ID or username",
|
|
@@ -1188,11 +1491,7 @@ Examples:
|
|
|
1188
1491
|
.command("add-bot")
|
|
1189
1492
|
.description("Add SaiTeam_bot and promote it to admin when possible")
|
|
1190
1493
|
.option("--profile <name>", "Profile name to use for Telegram credentials")
|
|
1191
|
-
.requiredOption(
|
|
1192
|
-
"--chat <id>",
|
|
1193
|
-
"Telegram chat ID or username",
|
|
1194
|
-
parsePeer,
|
|
1195
|
-
)
|
|
1494
|
+
.requiredOption("--chat <id>", "Telegram chat ID or username", parsePeer)
|
|
1196
1495
|
.option(
|
|
1197
1496
|
"--target-user <id>",
|
|
1198
1497
|
"Bot user ID used for membership audit",
|
|
@@ -1214,11 +1513,7 @@ Examples:
|
|
|
1214
1513
|
.command("add-owner")
|
|
1215
1514
|
.description("Add a target user as owner if possible, otherwise admin")
|
|
1216
1515
|
.option("--profile <name>", "Profile name to use for Telegram credentials")
|
|
1217
|
-
.requiredOption(
|
|
1218
|
-
"--chat <id>",
|
|
1219
|
-
"Telegram chat ID or username",
|
|
1220
|
-
parsePeer,
|
|
1221
|
-
)
|
|
1516
|
+
.requiredOption("--chat <id>", "Telegram chat ID or username", parsePeer)
|
|
1222
1517
|
.requiredOption(
|
|
1223
1518
|
"--target-user <peer>",
|
|
1224
1519
|
"Target user ID or username",
|
|
@@ -1265,6 +1560,11 @@ Examples:
|
|
|
1265
1560
|
"--no-transfer-owner",
|
|
1266
1561
|
"Skip ownership transfer even when TELEGRAM_PASSWORD is available",
|
|
1267
1562
|
)
|
|
1563
|
+
.option(
|
|
1564
|
+
"--target-user <peer>",
|
|
1565
|
+
"Owner target user ID or username; default is the Sai custody account",
|
|
1566
|
+
parsePeer,
|
|
1567
|
+
)
|
|
1268
1568
|
.action(async (options: SaiBootstrapOptions) => {
|
|
1269
1569
|
await runSaiBootstrap(options)
|
|
1270
1570
|
})
|
|
@@ -1272,6 +1572,10 @@ Examples:
|
|
|
1272
1572
|
const profile = program
|
|
1273
1573
|
.command("profile")
|
|
1274
1574
|
.description("Manage local Telegram session profiles")
|
|
1575
|
+
.action(async () => {
|
|
1576
|
+
profile.outputHelp()
|
|
1577
|
+
await printConfiguredProfiles()
|
|
1578
|
+
})
|
|
1275
1579
|
|
|
1276
1580
|
profile
|
|
1277
1581
|
.command("find")
|
|
@@ -1287,10 +1591,13 @@ Examples:
|
|
|
1287
1591
|
await runProfileList()
|
|
1288
1592
|
})
|
|
1289
1593
|
|
|
1290
|
-
profile
|
|
1594
|
+
const addProfileCommand = profile
|
|
1291
1595
|
.command("add")
|
|
1292
1596
|
.description("Add or update a Telegram session profile")
|
|
1293
|
-
.
|
|
1597
|
+
.option(
|
|
1598
|
+
"--name <name>",
|
|
1599
|
+
"Local profile name; use a distinct name for another account",
|
|
1600
|
+
)
|
|
1294
1601
|
.option("--api-id <id>", "Telegram API ID")
|
|
1295
1602
|
.option("--api-hash <hash>", "Telegram API hash")
|
|
1296
1603
|
.option("--session-string <value>", "Telegram session string")
|
|
@@ -1298,43 +1605,78 @@ Examples:
|
|
|
1298
1605
|
"--password <value>",
|
|
1299
1606
|
"Telegram 2FA password for ownership transfer",
|
|
1300
1607
|
)
|
|
1301
|
-
.option("--src <number>", "
|
|
1302
|
-
.option(
|
|
1303
|
-
|
|
1304
|
-
|
|
1608
|
+
.option("--src <number>", "Import a numbered credential source from profile find, not a saved profile")
|
|
1609
|
+
.option(
|
|
1610
|
+
"--login",
|
|
1611
|
+
"Start a fresh account login, ignoring discovered sessions (defaults to QR)",
|
|
1612
|
+
)
|
|
1613
|
+
.option("--qr", "Generate a session string with QR login")
|
|
1614
|
+
.option("--phone", "Generate a session string with phone login")
|
|
1615
|
+
.option("-i, --interactive", "Prompt for missing profile values")
|
|
1305
1616
|
.option("--handle <handle>", "Resolved Telegram handle")
|
|
1306
1617
|
.option("--user-id <id>", "Resolved Telegram user ID")
|
|
1307
1618
|
.option("--display-name <name>", "Resolved Telegram display name")
|
|
1619
|
+
.addHelpText(
|
|
1620
|
+
"after",
|
|
1621
|
+
`
|
|
1622
|
+
Examples:
|
|
1623
|
+
tg-sai profile add --name sai-custody --login --qr --interactive
|
|
1624
|
+
tg-sai profile add --name another-account --phone --interactive
|
|
1625
|
+
tg-sai profile add --name another-account --src 1
|
|
1626
|
+
tg-sai profile add --name existing-profile --password '<password>'
|
|
1627
|
+
|
|
1628
|
+
If apiId/apiHash are missing, create a Telegram API app at:
|
|
1629
|
+
https://my.telegram.org/apps
|
|
1630
|
+
`,
|
|
1631
|
+
)
|
|
1308
1632
|
.action(async (options: ProfileAddOptions) => {
|
|
1633
|
+
if (Object.keys(options).length === 0) {
|
|
1634
|
+
await printProfileAddHelp(addProfileCommand)
|
|
1635
|
+
return
|
|
1636
|
+
}
|
|
1309
1637
|
await runProfileAdd(options)
|
|
1310
1638
|
})
|
|
1311
1639
|
|
|
1312
1640
|
profile
|
|
1313
1641
|
.command("show")
|
|
1314
1642
|
.description("Show one configured profile")
|
|
1315
|
-
.argument(
|
|
1316
|
-
|
|
1317
|
-
|
|
1643
|
+
.argument(
|
|
1644
|
+
"[selector]",
|
|
1645
|
+
"Profile index, handle, user ID, or legacy name",
|
|
1646
|
+
"0",
|
|
1647
|
+
)
|
|
1648
|
+
.action(async (selector: string) => {
|
|
1649
|
+
await runProfileShow(selector)
|
|
1318
1650
|
})
|
|
1319
1651
|
|
|
1320
1652
|
profile
|
|
1321
1653
|
.command("use")
|
|
1322
1654
|
.description("Set the active profile")
|
|
1323
|
-
.argument("<
|
|
1324
|
-
.action(async (
|
|
1325
|
-
await setActiveProfile(
|
|
1326
|
-
|
|
1655
|
+
.argument("<selector>", "Profile index, handle, user ID, or legacy name")
|
|
1656
|
+
.action(async (selector: string) => {
|
|
1657
|
+
const config = await setActiveProfile(selector)
|
|
1658
|
+
const active = config.profiles[0]!
|
|
1659
|
+
console.log(`active_profile: ${profileLabel(active, 0)}`)
|
|
1327
1660
|
})
|
|
1328
1661
|
|
|
1329
|
-
profile
|
|
1662
|
+
const removeProfileCommand = profile
|
|
1330
1663
|
.command("remove")
|
|
1331
1664
|
.description("Remove one configured profile")
|
|
1332
|
-
.argument(
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1665
|
+
.argument(
|
|
1666
|
+
"[selector]",
|
|
1667
|
+
"Profile index, handle, user ID, or legacy name; omit for examples",
|
|
1668
|
+
)
|
|
1669
|
+
.action(async (selector: string | undefined) => {
|
|
1670
|
+
if (selector === undefined) {
|
|
1671
|
+
await printProfileRemoveHelp(removeProfileCommand)
|
|
1672
|
+
return
|
|
1673
|
+
}
|
|
1674
|
+
await removeProfile(selector)
|
|
1675
|
+
console.log(`removed_profile: ${selector}`)
|
|
1336
1676
|
})
|
|
1337
1677
|
|
|
1678
|
+
addProvisionCommands(program)
|
|
1679
|
+
addCustodyWorkerCommands(program)
|
|
1338
1680
|
return program
|
|
1339
1681
|
}
|
|
1340
1682
|
|