@uniquedivine/tg-sai 0.0.3 → 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 +32 -51
- package/main.ts +460 -106
- 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/README.md
CHANGED
|
@@ -5,6 +5,30 @@ Sai Telegram admin audit and bootstrap CLI.
|
|
|
5
5
|
See file `RUNBOOK.md` for operator setup, profile management, and safe
|
|
6
6
|
execution workflows.
|
|
7
7
|
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Requires [Bun](https://bun.sh). No clone needed:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
bunx @uniquedivine/tg-sai --help # run once
|
|
14
|
+
bun add -g @uniquedivine/tg-sai # or install the `tg-sai` command
|
|
15
|
+
tg-sai profile add --name me --login --qr --interactive
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Move Sai chats you own to the custody account and add SaiTeam_bot as admin.
|
|
19
|
+
The command is a dry run until you add `--run`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
tg-sai ops sai-bootstrap --file chats.txt # plan only
|
|
23
|
+
tg-sai ops sai-bootstrap --file chats.txt --run # apply
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
File `chats.txt` holds one chat ID per line. Ownership transfer needs your
|
|
27
|
+
Telegram 2FA password in `TELEGRAM_PASSWORD` or the saved profile. The owner
|
|
28
|
+
target defaults to the Sai custody account,
|
|
29
|
+
[@SaiTeam_AI](https://t.me/SaiTeam_AI) (user 8274304175), and the bot is
|
|
30
|
+
[@SaiTeam_bot](https://t.me/SaiTeam_bot). `--target-user` overrides the owner.
|
|
31
|
+
|
|
8
32
|
## Development
|
|
9
33
|
|
|
10
34
|
```bash
|
package/RUNBOOK.md
CHANGED
|
@@ -35,8 +35,15 @@ Credential profile behavior:
|
|
|
35
35
|
|
|
36
36
|
- Command `tg-sai profile find` reports discovered Telegram credential sources
|
|
37
37
|
without writing profile state.
|
|
38
|
-
-
|
|
39
|
-
|
|
38
|
+
- Bare commands `tg-sai profile` and `tg-sai profile add` show help and configured
|
|
39
|
+
profiles without saving state or connecting to Telegram.
|
|
40
|
+
- Command `tg-sai profile add` with explicit options stores a local profile from
|
|
41
|
+
flags, process environment variables, or merged non-conflicting discovered
|
|
42
|
+
sources. Use a distinct `--name` for another account; without it, explicit
|
|
43
|
+
options update the active/default profile.
|
|
44
|
+
- Explicit `--qr`, `--phone`, or `--login` starts a fresh account login and
|
|
45
|
+
ignores discovered/environment sessions and account passwords. App credentials
|
|
46
|
+
can still come from discovery, environment, or interactive input.
|
|
40
47
|
- Commands `tg-sai profile list`, `tg-sai profile show`, `tg-sai profile use`,
|
|
41
48
|
and `tg-sai profile remove` manage local session profiles.
|
|
42
49
|
|
|
@@ -102,11 +109,12 @@ cd /path/to/tg-sai
|
|
|
102
109
|
uv sync
|
|
103
110
|
```
|
|
104
111
|
|
|
105
|
-
Create file
|
|
106
|
-
credentials:
|
|
112
|
+
Create user-local file `~/.tg-sai/.env` with Telegram app credentials:
|
|
107
113
|
|
|
108
114
|
```bash
|
|
109
|
-
|
|
115
|
+
mkdir -p ~/.tg-sai
|
|
116
|
+
chmod 700 ~/.tg-sai
|
|
117
|
+
cat > ~/.tg-sai/.env <<'EOF'
|
|
110
118
|
TELEGRAM_API_ID=<api id from https://my.telegram.org/apps>
|
|
111
119
|
TELEGRAM_API_HASH=<api hash from https://my.telegram.org/apps>
|
|
112
120
|
EOF
|
|
@@ -121,18 +129,20 @@ The session string is generated after QR login.
|
|
|
121
129
|
Generate the session string and save it directly into a `tg-sai` profile:
|
|
122
130
|
|
|
123
131
|
```bash
|
|
124
|
-
tg-sai profile add --name
|
|
132
|
+
tg-sai profile add --name sai-custody --login --qr --interactive
|
|
125
133
|
```
|
|
126
134
|
|
|
127
135
|
Scan the QR code in Telegram with **Settings > Devices > Link Desktop Device**.
|
|
128
|
-
|
|
129
|
-
`
|
|
136
|
+
Scan while signed into the intended account. After login succeeds, this command
|
|
137
|
+
saves its verified identity and session in the separate `sai-custody` profile.
|
|
138
|
+
The existing active profile stays selected. Inspect the result with command
|
|
139
|
+
`tg-sai profile show sai-custody`.
|
|
130
140
|
|
|
131
141
|
If you generated the session string manually, save it with explicit flags:
|
|
132
142
|
|
|
133
143
|
```bash
|
|
134
144
|
tg-sai profile add \
|
|
135
|
-
--name
|
|
145
|
+
--name sai-custody \
|
|
136
146
|
--api-id <api id> \
|
|
137
147
|
--api-hash <api hash> \
|
|
138
148
|
--session-string '<session string>'
|
|
@@ -142,7 +152,7 @@ Store the Telegram 2FA password in the same profile when ownership transfer is
|
|
|
142
152
|
needed:
|
|
143
153
|
|
|
144
154
|
```bash
|
|
145
|
-
tg-sai profile add --name
|
|
155
|
+
tg-sai profile add --name sai-custody --password '<password>'
|
|
146
156
|
```
|
|
147
157
|
|
|
148
158
|
Check the local setup:
|
|
@@ -169,17 +179,26 @@ Add a local profile from a numbered source shown by command
|
|
|
169
179
|
`tg-sai profile find`:
|
|
170
180
|
|
|
171
181
|
```bash
|
|
172
|
-
tg-sai profile add --name
|
|
182
|
+
tg-sai profile add --name another-account --src 1
|
|
173
183
|
```
|
|
174
184
|
|
|
175
185
|
Command `tg-sai profile add` can store the Telegram 2FA password with flag
|
|
176
186
|
`--password`. Command `tg-sai profile show` redacts stored `apiHash`,
|
|
177
187
|
`sessionString`, and `password`.
|
|
178
188
|
|
|
189
|
+
For password or metadata updates, `--name` selects an existing saved profile by
|
|
190
|
+
name, handle, numeric user ID, or saved-list index. The update preserves that
|
|
191
|
+
profile's account identity and session without importing environment or discovered
|
|
192
|
+
credentials. An unknown password-update selector fails instead of creating a
|
|
193
|
+
profile. Explicit login/import cannot replace a known profile with a different
|
|
194
|
+
account; use a new name for another account, or remove the old profile first.
|
|
195
|
+
Flag `--src` selects a credential source numbered by `tg-sai profile find`, not
|
|
196
|
+
the saved-profile index printed by `tg-sai profile list`.
|
|
197
|
+
|
|
179
198
|
Create a blank draft profile for onboarding:
|
|
180
199
|
|
|
181
200
|
```bash
|
|
182
|
-
tg-sai profile add --name
|
|
201
|
+
tg-sai profile add --name draft
|
|
183
202
|
```
|
|
184
203
|
|
|
185
204
|
Blank or partial profiles are useful while collecting `apiId`, `apiHash`, and
|
|
@@ -190,7 +209,7 @@ Add a local profile from explicit values:
|
|
|
190
209
|
|
|
191
210
|
```bash
|
|
192
211
|
tg-sai profile add \
|
|
193
|
-
--name
|
|
212
|
+
--name another-account \
|
|
194
213
|
--handle <telegram-handle> \
|
|
195
214
|
--user-id <telegram-user-id> \
|
|
196
215
|
--display-name "<display name>"
|
|
@@ -202,6 +221,11 @@ List local profiles:
|
|
|
202
221
|
tg-sai profile list
|
|
203
222
|
```
|
|
204
223
|
|
|
224
|
+
Command `tg-sai profile` also shows configured profiles beneath its command
|
|
225
|
+
list. Bare command `tg-sai profile add` explains fresh QR enrollment and how
|
|
226
|
+
named accounts differ from updates to the active profile. Neither changes the
|
|
227
|
+
saved configuration.
|
|
228
|
+
|
|
205
229
|
Find Sai-related chats visible to the active Telegram session:
|
|
206
230
|
|
|
207
231
|
```bash
|
|
@@ -308,10 +332,10 @@ different user is the creator.
|
|
|
308
332
|
|
|
309
333
|
## Suggested workflow
|
|
310
334
|
|
|
311
|
-
1. Run command `
|
|
335
|
+
1. Run command `tg-sai profile find` to inspect available Telegram
|
|
312
336
|
credential sources.
|
|
313
|
-
2. Run command `
|
|
314
|
-
|
|
337
|
+
2. Run command `tg-sai profile add --qr` to save a local profile and generate a
|
|
338
|
+
missing session string.
|
|
315
339
|
3. Run command `tg-sai audit --chat <id>` before writes.
|
|
316
340
|
4. Run command `tg-sai add-bot --chat <id>` or command
|
|
317
341
|
`tg-sai add-owner --chat <id> --target-user <user>` as a dry-run.
|
|
@@ -341,3 +365,100 @@ list regular members as admins. This tool uses per-member lookups
|
|
|
341
365
|
- Emit a reviewable table or JSON artifact for batch dry-runs.
|
|
342
366
|
- Add named rights profiles for bot admin and owner-target admin promotion.
|
|
343
367
|
- Add fixtures for a completed ownership transfer.
|
|
368
|
+
|
|
369
|
+
## Custody Chat Provisioning
|
|
370
|
+
|
|
371
|
+
SaiTeam_bot's private `/newdealchat` flow submits an approved request to the
|
|
372
|
+
Deal Desk API. A separate worker creates the private supergroup as custody
|
|
373
|
+
user `8274304175`, profile `SaiTeam_AI`. The bot, Unique, and the requester
|
|
374
|
+
become admins. August and Erick are ordinary default members. The deployed bot
|
|
375
|
+
configuration controls the requester allowlist and defaults.
|
|
376
|
+
|
|
377
|
+
When adding an operator, configure their numeric Telegram user ID in the bot
|
|
378
|
+
allowlist. A fresh custody session may not know that ID's access hash. Add a
|
|
379
|
+
verified handle alias in `provisioningRuntime.ts` for new team operators and
|
|
380
|
+
deploy the worker with it. Resolve the handle through the custody account first
|
|
381
|
+
and verify the exact numeric ID and that it is a user account. The runtime
|
|
382
|
+
rejects handles that later resolve to a different account.
|
|
383
|
+
|
|
384
|
+
For explicit CLI requests, prepare a JSON file with a stable `requestId`,
|
|
385
|
+
`custodyProfile`, `expectedUserId`, `title`, `participants`, `admins`,
|
|
386
|
+
`botRightsProfile`, and `dealIds`. Each admin uses `rightsProfile: "team"`;
|
|
387
|
+
the bot uses `botRightsProfile: "baseline"`. `optionalParticipants` must be a
|
|
388
|
+
subset of ordinary participants. `dealIds: []` registers a chat without
|
|
389
|
+
creating or linking a commercial deal.
|
|
390
|
+
|
|
391
|
+
```sh
|
|
392
|
+
tg-sai chat provision prepare --file request.json
|
|
393
|
+
tg-sai chat provision prepare --file request.json --run
|
|
394
|
+
tg-sai chat provision run --request-id <request-id>
|
|
395
|
+
tg-sai chat provision run --request-id <request-id> --run
|
|
396
|
+
tg-sai chat provision status --request-id <request-id>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The cloud CLI credential needs the corresponding chat workflow capabilities.
|
|
400
|
+
The background worker uses a dedicated read-only record credential with only
|
|
401
|
+
`chats:work` granted for workflow writes. The worker service gets only the
|
|
402
|
+
custody profile, without personal sessions or ownership-transfer passwords.
|
|
403
|
+
|
|
404
|
+
```sh
|
|
405
|
+
tg-sai worker --custody-profile SaiTeam_AI --once
|
|
406
|
+
tg-sai worker --custody-profile SaiTeam_AI --once --run
|
|
407
|
+
tg-sai worker --custody-profile SaiTeam_AI --run --interval-ms 5000
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Without `--run`, the worker reads one queue page and does not connect to
|
|
411
|
+
Telegram. Execution uses a durable worker identity, account lock, and local
|
|
412
|
+
receipts. Never delete these files to force a retry. An uncertain creation
|
|
413
|
+
blocks further creation until an operator reconciles the original request.
|
|
414
|
+
Use `chat provision resolve --help` to adopt a verified group with its original
|
|
415
|
+
creation marker. Known-group partial setup resumes against that same group.
|
|
416
|
+
|
|
417
|
+
Required admins must be members before setup becomes ready. An ordinary member
|
|
418
|
+
blocked by privacy settings can remain pending with a 24-hour, approval-required
|
|
419
|
+
invite link. A link does not mean the person joined. Handle-based resolution of
|
|
420
|
+
fixed team defaults must verify the expected numeric ID before any invitation.
|
|
421
|
+
|
|
422
|
+
New ready receipts include an `openGroupUrl` anchored to a message actually
|
|
423
|
+
observed in the group and the existing `primaryInviteLink`, when available.
|
|
424
|
+
These read-only lookups run together with a 10-second limit each. A missing link
|
|
425
|
+
does not undo verified setup. Existing primary invitations are never rotated or
|
|
426
|
+
created by this step, and privacy-blocked members keep their separate controlled
|
|
427
|
+
invitations. The bot displays the saved links in its completion reply and
|
|
428
|
+
`/chatstatus`. Completed requests are not rewritten to refresh missing links.
|
|
429
|
+
|
|
430
|
+
The source bot configuration starts with admission in `observe` mode. Check
|
|
431
|
+
`~/ki/boku/epics/comms/telegram-bot.md` and its dated rollout evidence for
|
|
432
|
+
the actual deployed state before testing bot commands.
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
## Runtime Health and Deployment Readiness
|
|
436
|
+
|
|
437
|
+
`tg-sai health` now defaults to offline runtime checks. It verifies both bundled
|
|
438
|
+
WASM assets, crypto and compression operations, and actual Telegram client
|
|
439
|
+
preparation with a temporary SQLite store. It does not read profiles or contact
|
|
440
|
+
Telegram. `--json` provides a machine-readable result and failures exit nonzero.
|
|
441
|
+
|
|
442
|
+
```sh
|
|
443
|
+
tg-sai health --offline --json
|
|
444
|
+
tg-sai health --online --custody-profile SaiTeam_AI --json
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Online mode additionally strict-loads custody user `8274304175`, acquires the
|
|
448
|
+
same account lock used by provisioning, and performs bounded `getMe` validation.
|
|
449
|
+
It cannot fall back to a personal account and does not create or modify chats.
|
|
450
|
+
An account-lock conflict fails the check; run it while the worker is stopped or
|
|
451
|
+
idle. The earlier active-profile/workstation diagnostics remain available with
|
|
452
|
+
`tg-sai health --local` and are separate from custody-worker readiness.
|
|
453
|
+
|
|
454
|
+
The deployed executable includes a worker command and the same health commands.
|
|
455
|
+
A release candidate must pass offline health as the worker Unix user before
|
|
456
|
+
installation selects it. The worker systemd unit runs offline and online checks
|
|
457
|
+
before starting the queue loop. A service being alive, `--help`, or an empty
|
|
458
|
+
queue preview does not prove Telegram client initialization.
|
|
459
|
+
|
|
460
|
+
From the Boku Deal Desk source directory, `just bot-health` checks the deployed
|
|
461
|
+
runtime under its service sandbox. `just bot-health --online` also validates
|
|
462
|
+
custody identity. These are operator probes, not the API's `/health` endpoint.
|
|
463
|
+
That endpoint reports API health and whether bot ingress is configured; it does
|
|
464
|
+
not certify Telegram custody readiness.
|
package/actionExecutor.ts
CHANGED
|
@@ -8,54 +8,54 @@
|
|
|
8
8
|
export type BatchActionEvent =
|
|
9
9
|
| {
|
|
10
10
|
/** Operation attempt started. */
|
|
11
|
-
type: "action_start"
|
|
11
|
+
type: "action_start"
|
|
12
12
|
/** Telegram chat ID or peer for the attempted action. */
|
|
13
|
-
chatId: number | string
|
|
13
|
+
chatId: number | string
|
|
14
14
|
/** Stable action name, for example `bot_add`. */
|
|
15
|
-
action: string
|
|
15
|
+
action: string
|
|
16
16
|
/** One-based attempt number for this action. */
|
|
17
|
-
attempt: number
|
|
17
|
+
attempt: number
|
|
18
18
|
}
|
|
19
19
|
| {
|
|
20
20
|
/** Telegram returned `FLOOD_WAIT_X` and the executor will retry later. */
|
|
21
|
-
type: "flood_wait"
|
|
21
|
+
type: "flood_wait"
|
|
22
22
|
/** Telegram chat ID or peer for the rate-limited action. */
|
|
23
|
-
chatId: number | string
|
|
23
|
+
chatId: number | string
|
|
24
24
|
/** Stable action name, for example `bot_promote_admin`. */
|
|
25
|
-
action: string
|
|
25
|
+
action: string
|
|
26
26
|
/** One-based attempt number that received the flood-wait error. */
|
|
27
|
-
attempt: number
|
|
27
|
+
attempt: number
|
|
28
28
|
/** Seconds Telegram requested in `FLOOD_WAIT_X`. */
|
|
29
|
-
waitSeconds: number
|
|
29
|
+
waitSeconds: number
|
|
30
30
|
/** Milliseconds the executor sleeps, including configured buffer. */
|
|
31
|
-
sleepMs: number
|
|
31
|
+
sleepMs: number
|
|
32
32
|
}
|
|
33
33
|
| {
|
|
34
34
|
/** Operation completed successfully. */
|
|
35
|
-
type: "action_success"
|
|
35
|
+
type: "action_success"
|
|
36
36
|
/** Telegram chat ID or peer for the successful action. */
|
|
37
|
-
chatId: number | string
|
|
37
|
+
chatId: number | string
|
|
38
38
|
/** Stable action name, for example `owner_target_add`. */
|
|
39
|
-
action: string
|
|
39
|
+
action: string
|
|
40
40
|
/** Number of attempts required before success. */
|
|
41
|
-
attempts: number
|
|
41
|
+
attempts: number
|
|
42
42
|
/** Elapsed milliseconds from first attempt to success. */
|
|
43
|
-
elapsedMs: number
|
|
43
|
+
elapsedMs: number
|
|
44
44
|
}
|
|
45
45
|
| {
|
|
46
46
|
/** Operation failed with a non-retryable error or exhausted retries. */
|
|
47
|
-
type: "action_error"
|
|
47
|
+
type: "action_error"
|
|
48
48
|
/** Telegram chat ID or peer for the failed action. */
|
|
49
|
-
chatId: number | string
|
|
49
|
+
chatId: number | string
|
|
50
50
|
/** Stable action name, for example `transfer_owner`. */
|
|
51
|
-
action: string
|
|
51
|
+
action: string
|
|
52
52
|
/** One-based attempt number that produced the final error. */
|
|
53
|
-
attempt: number
|
|
53
|
+
attempt: number
|
|
54
54
|
/** Error name normalized for logs. */
|
|
55
|
-
errorName: string
|
|
55
|
+
errorName: string
|
|
56
56
|
/** Error message normalized for logs. */
|
|
57
|
-
errorMessage: string
|
|
58
|
-
}
|
|
57
|
+
errorMessage: string
|
|
58
|
+
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Receives structured batch action events.
|
|
@@ -64,7 +64,7 @@ export type BatchActionEvent =
|
|
|
64
64
|
* Tests can use `MemoryBatchLogger` to inspect event order without parsing text.
|
|
65
65
|
*/
|
|
66
66
|
export interface BatchLogger {
|
|
67
|
-
event(event: BatchActionEvent): void | Promise<void
|
|
67
|
+
event(event: BatchActionEvent): void | Promise<void>
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -72,9 +72,9 @@ export interface BatchLogger {
|
|
|
72
72
|
*/
|
|
73
73
|
export interface TelegramActionContext {
|
|
74
74
|
/** Telegram chat ID or resolvable chat peer for the action. */
|
|
75
|
-
chatId: number | string
|
|
75
|
+
chatId: number | string
|
|
76
76
|
/** Stable action name such as `bot_add` or `owner_target_promote_admin`. */
|
|
77
|
-
action: string
|
|
77
|
+
action: string
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
@@ -85,28 +85,28 @@ export interface TelegramActionContext {
|
|
|
85
85
|
*/
|
|
86
86
|
export interface TelegramActionExecutorDeps {
|
|
87
87
|
/** Event sink for action start, success, error, and flood-wait events. */
|
|
88
|
-
logger: BatchLogger
|
|
88
|
+
logger: BatchLogger
|
|
89
89
|
/** Sleep function used after Telegram returns `FLOOD_WAIT_X`. */
|
|
90
|
-
sleep: (ms: number) => Promise<void
|
|
90
|
+
sleep: (ms: number) => Promise<void>
|
|
91
91
|
/** Clock function used to compute elapsed action time. */
|
|
92
|
-
now: () => number
|
|
92
|
+
now: () => number
|
|
93
93
|
/** Number of retries after the first failed attempt. */
|
|
94
|
-
maxRetries: number
|
|
94
|
+
maxRetries: number
|
|
95
95
|
/** Extra milliseconds added to Telegram's required flood-wait duration. */
|
|
96
|
-
bufferMs: number
|
|
96
|
+
bufferMs: number
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Test logger that stores every event in insertion order.
|
|
101
101
|
*/
|
|
102
102
|
export class MemoryBatchLogger implements BatchLogger {
|
|
103
|
-
readonly events: BatchActionEvent[] = []
|
|
103
|
+
readonly events: BatchActionEvent[] = []
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Appends an event to the in-memory event list.
|
|
107
107
|
*/
|
|
108
108
|
event(event: BatchActionEvent): void {
|
|
109
|
-
this.events.push(event)
|
|
109
|
+
this.events.push(event)
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -120,16 +120,16 @@ export class MemoryBatchLogger implements BatchLogger {
|
|
|
120
120
|
*/
|
|
121
121
|
export function parseFloodWaitSeconds(error: unknown): number | null {
|
|
122
122
|
if (!(error instanceof Error)) {
|
|
123
|
-
return null
|
|
123
|
+
return null
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
const haystack = `${error.name} ${error.message}
|
|
127
|
-
const match = haystack.match(/FLOOD_WAIT_(\d+)/)
|
|
126
|
+
const haystack = `${error.name} ${error.message}`
|
|
127
|
+
const match = haystack.match(/FLOOD_WAIT_(\d+)/)
|
|
128
128
|
if (!match) {
|
|
129
|
-
return null
|
|
129
|
+
return null
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
return Number(match[1])
|
|
132
|
+
return Number(match[1])
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/**
|
|
@@ -137,9 +137,9 @@ export function parseFloodWaitSeconds(error: unknown): number | null {
|
|
|
137
137
|
*/
|
|
138
138
|
export function errorName(error: unknown): string {
|
|
139
139
|
if (error instanceof Error && error.name) {
|
|
140
|
-
return error.name
|
|
140
|
+
return error.name
|
|
141
141
|
}
|
|
142
|
-
return "UnknownError"
|
|
142
|
+
return "UnknownError"
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
@@ -147,9 +147,9 @@ export function errorName(error: unknown): string {
|
|
|
147
147
|
*/
|
|
148
148
|
export function errorMessage(error: unknown): string {
|
|
149
149
|
if (error instanceof Error) {
|
|
150
|
-
return error.message
|
|
150
|
+
return error.message
|
|
151
151
|
}
|
|
152
|
-
return String(error)
|
|
152
|
+
return String(error)
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/**
|
|
@@ -165,8 +165,8 @@ export async function withFloodWaitRetry<T>(
|
|
|
165
165
|
operation: () => Promise<T>,
|
|
166
166
|
deps: TelegramActionExecutorDeps,
|
|
167
167
|
): Promise<T> {
|
|
168
|
-
const startedAt = deps.now()
|
|
169
|
-
const maxAttempts = deps.maxRetries + 1
|
|
168
|
+
const startedAt = deps.now()
|
|
169
|
+
const maxAttempts = deps.maxRetries + 1
|
|
170
170
|
|
|
171
171
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
172
172
|
await deps.logger.event({
|
|
@@ -174,21 +174,21 @@ export async function withFloodWaitRetry<T>(
|
|
|
174
174
|
chatId: context.chatId,
|
|
175
175
|
action: context.action,
|
|
176
176
|
attempt,
|
|
177
|
-
})
|
|
177
|
+
})
|
|
178
178
|
|
|
179
179
|
try {
|
|
180
|
-
const result = await operation()
|
|
180
|
+
const result = await operation()
|
|
181
181
|
await deps.logger.event({
|
|
182
182
|
type: "action_success",
|
|
183
183
|
chatId: context.chatId,
|
|
184
184
|
action: context.action,
|
|
185
185
|
attempts: attempt,
|
|
186
186
|
elapsedMs: deps.now() - startedAt,
|
|
187
|
-
})
|
|
188
|
-
return result
|
|
187
|
+
})
|
|
188
|
+
return result
|
|
189
189
|
} catch (error) {
|
|
190
|
-
const waitSeconds = parseFloodWaitSeconds(error)
|
|
191
|
-
const canRetry = waitSeconds !== null && attempt < maxAttempts
|
|
190
|
+
const waitSeconds = parseFloodWaitSeconds(error)
|
|
191
|
+
const canRetry = waitSeconds !== null && attempt < maxAttempts
|
|
192
192
|
|
|
193
193
|
if (!canRetry) {
|
|
194
194
|
await deps.logger.event({
|
|
@@ -198,11 +198,11 @@ export async function withFloodWaitRetry<T>(
|
|
|
198
198
|
attempt,
|
|
199
199
|
errorName: errorName(error),
|
|
200
200
|
errorMessage: errorMessage(error),
|
|
201
|
-
})
|
|
202
|
-
throw error
|
|
201
|
+
})
|
|
202
|
+
throw error
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
const sleepMs = waitSeconds * 1000 + deps.bufferMs
|
|
205
|
+
const sleepMs = waitSeconds * 1000 + deps.bufferMs
|
|
206
206
|
await deps.logger.event({
|
|
207
207
|
type: "flood_wait",
|
|
208
208
|
chatId: context.chatId,
|
|
@@ -210,10 +210,10 @@ export async function withFloodWaitRetry<T>(
|
|
|
210
210
|
attempt,
|
|
211
211
|
waitSeconds,
|
|
212
212
|
sleepMs,
|
|
213
|
-
})
|
|
214
|
-
await deps.sleep(sleepMs)
|
|
213
|
+
})
|
|
214
|
+
await deps.sleep(sleepMs)
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
throw new Error(`unreachable action executor state: ${context.action}`)
|
|
218
|
+
throw new Error(`unreachable action executor state: ${context.action}`)
|
|
219
219
|
}
|