@will-17173/telegram-cli 0.3.0 → 0.4.0
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 +185 -61
- package/README.zh-CN.md +199 -77
- package/dist/account/account-authenticator.js +95 -0
- package/dist/account/account-authenticator.js.map +1 -0
- package/dist/account/account-context.js +7 -0
- package/dist/account/account-context.js.map +1 -1
- package/dist/account/account-name.js +17 -0
- package/dist/account/account-name.js.map +1 -0
- package/dist/account/account-presets.js +20 -3
- package/dist/account/account-presets.js.map +1 -1
- package/dist/account/account-store.js +277 -37
- package/dist/account/account-store.js.map +1 -1
- package/dist/cli/app.js +12 -1
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/output.js +21 -6
- package/dist/cli/output.js.map +1 -1
- package/dist/cli/secure-input.js +408 -0
- package/dist/cli/secure-input.js.map +1 -0
- package/dist/commands/account-options.js +37 -7
- package/dist/commands/account-options.js.map +1 -1
- package/dist/commands/account.js +144 -62
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/archive.js +151 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/config.js +119 -12
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contact.js +58 -0
- package/dist/commands/contact.js.map +1 -0
- package/dist/commands/data.js +14 -7
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/dialog.js +156 -0
- package/dist/commands/dialog.js.map +1 -0
- package/dist/commands/folder.js +113 -0
- package/dist/commands/folder.js.map +1 -0
- package/dist/commands/group-write.js +45 -5
- package/dist/commands/group-write.js.map +1 -1
- package/dist/commands/group.js +23 -1
- package/dist/commands/group.js.map +1 -1
- package/dist/commands/notification.js +76 -0
- package/dist/commands/notification.js.map +1 -0
- package/dist/commands/query.js +22 -15
- package/dist/commands/query.js.map +1 -1
- package/dist/commands/telegram-runner.js +40 -18
- package/dist/commands/telegram-runner.js.map +1 -1
- package/dist/commands/telegram.js +82 -38
- package/dist/commands/telegram.js.map +1 -1
- package/dist/commands/time-range.js +39 -0
- package/dist/commands/time-range.js.map +1 -0
- package/dist/commands/types.js +3 -2
- package/dist/commands/types.js.map +1 -1
- package/dist/config/credential-store.js +24 -4
- package/dist/config/credential-store.js.map +1 -1
- package/dist/config/env.js +4 -1
- package/dist/config/env.js.map +1 -1
- package/dist/group-commands/catalog.js +10 -0
- package/dist/group-commands/catalog.js.map +1 -1
- package/dist/group-commands/executor.js +13 -4
- package/dist/group-commands/executor.js.map +1 -1
- package/dist/presenters/group.js +16 -0
- package/dist/presenters/group.js.map +1 -1
- package/dist/presenters/human.js +113 -0
- package/dist/presenters/human.js.map +1 -1
- package/dist/presenters/ink/group-command-result.js +2 -0
- package/dist/presenters/ink/group-command-result.js.map +1 -1
- package/dist/presenters/ink/listen-scroll.js +4 -3
- package/dist/presenters/ink/listen-scroll.js.map +1 -1
- package/dist/presenters/ink/listen.js +163 -43
- package/dist/presenters/ink/listen.js.map +1 -1
- package/dist/presenters/ink/mouse-scroll.js +12 -0
- package/dist/presenters/ink/mouse-scroll.js.map +1 -1
- package/dist/presenters/ink/secure-input.js +68 -0
- package/dist/presenters/ink/secure-input.js.map +1 -0
- package/dist/presenters/ink/use-group-command.js +78 -17
- package/dist/presenters/ink/use-group-command.js.map +1 -1
- package/dist/presenters/markdown.js +54 -0
- package/dist/presenters/markdown.js.map +1 -0
- package/dist/presenters/structured.js +8 -3
- package/dist/presenters/structured.js.map +1 -1
- package/dist/services/account-session-service.js +241 -0
- package/dist/services/account-session-service.js.map +1 -0
- package/dist/services/archive-layout.js +61 -0
- package/dist/services/archive-layout.js.map +1 -0
- package/dist/services/archive-manifest.js +187 -0
- package/dist/services/archive-manifest.js.map +1 -0
- package/dist/services/archive-markdown.js +383 -0
- package/dist/services/archive-markdown.js.map +1 -0
- package/dist/services/archive-service.js +718 -0
- package/dist/services/archive-service.js.map +1 -0
- package/dist/services/archive-types.js +2 -0
- package/dist/services/archive-types.js.map +1 -0
- package/dist/services/attachment-download.js +7 -2
- package/dist/services/attachment-download.js.map +1 -1
- package/dist/services/auto-download-coordinator.js +2 -4
- package/dist/services/auto-download-coordinator.js.map +1 -1
- package/dist/services/contact-service.js +78 -0
- package/dist/services/contact-service.js.map +1 -0
- package/dist/services/dialog-service.js +140 -0
- package/dist/services/dialog-service.js.map +1 -0
- package/dist/services/folder-service.js +200 -0
- package/dist/services/folder-service.js.map +1 -0
- package/dist/services/group-write-service.js +78 -18
- package/dist/services/group-write-service.js.map +1 -1
- package/dist/services/message-service.js +17 -1
- package/dist/services/message-service.js.map +1 -1
- package/dist/services/notification-service.js +142 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/services/write-access-policy.js +19 -0
- package/dist/services/write-access-policy.js.map +1 -0
- package/dist/telegram/archive-types.js +2 -0
- package/dist/telegram/archive-types.js.map +1 -0
- package/dist/telegram/client-factory.js +51 -11
- package/dist/telegram/client-factory.js.map +1 -1
- package/dist/telegram/contact-types.js +10 -0
- package/dist/telegram/contact-types.js.map +1 -0
- package/dist/telegram/dialog-types.js +2 -0
- package/dist/telegram/dialog-types.js.map +1 -0
- package/dist/telegram/errors.js +14 -0
- package/dist/telegram/errors.js.map +1 -0
- package/dist/telegram/fake-group-management.js +5 -0
- package/dist/telegram/fake-group-management.js.map +1 -1
- package/dist/telegram/folder-types.js +13 -0
- package/dist/telegram/folder-types.js.map +1 -0
- package/dist/telegram/group-write-types.js +28 -1
- package/dist/telegram/group-write-types.js.map +1 -1
- package/dist/telegram/mtcute-archive.js +190 -0
- package/dist/telegram/mtcute-archive.js.map +1 -0
- package/dist/telegram/mtcute-client.js +72 -11
- package/dist/telegram/mtcute-client.js.map +1 -1
- package/dist/telegram/mtcute-contacts.js +90 -0
- package/dist/telegram/mtcute-contacts.js.map +1 -0
- package/dist/telegram/mtcute-dialogs.js +186 -0
- package/dist/telegram/mtcute-dialogs.js.map +1 -0
- package/dist/telegram/mtcute-folders.js +318 -0
- package/dist/telegram/mtcute-folders.js.map +1 -0
- package/dist/telegram/mtcute-group-members.js +43 -3
- package/dist/telegram/mtcute-group-members.js.map +1 -1
- package/dist/telegram/mtcute-message-normalizer.js +63 -0
- package/dist/telegram/mtcute-message-normalizer.js.map +1 -0
- package/dist/telegram/mtcute-notifications.js +101 -0
- package/dist/telegram/mtcute-notifications.js.map +1 -0
- package/dist/telegram/notification-types.js +11 -0
- package/dist/telegram/notification-types.js.map +1 -0
- package/dist/telegram/types.js +6 -1
- package/dist/telegram/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,15 +5,15 @@ import stringWidth from 'string-width';
|
|
|
5
5
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { dirname } from 'node:path';
|
|
8
|
-
import { resolveAttachmentDestination } from '../../services/attachment-download.js';
|
|
8
|
+
import { attachmentDownloadProgress, resolveAttachmentDestination, } from '../../services/attachment-download.js';
|
|
9
9
|
import { ListenAlbumAggregator } from '../../services/listen-album-aggregator.js';
|
|
10
10
|
import { AutoDownloadCoordinator } from '../../services/auto-download-coordinator.js';
|
|
11
11
|
import { attachmentDownloadTarget, attachmentFileName, discoverListenAttachments, listenAttachmentKey } from '../../services/listen-attachment.js';
|
|
12
12
|
import { buildListenMessage } from '../listen-message.js';
|
|
13
|
-
import { applyMessageArrival, takeListenViewport } from './listen-scroll.js';
|
|
13
|
+
import { applyMessageArrival, applyScroll, takeListenViewport } from './listen-scroll.js';
|
|
14
14
|
import { decodeImagePreview } from './image-preview.js';
|
|
15
15
|
import { ListenScrollbar, calculateScrollbar, listenContentWidth, useTransientScrollbar } from './listen-scrollbar.js';
|
|
16
|
-
import {
|
|
16
|
+
import { isMouseInput, withAlternateScroll } from './mouse-scroll.js';
|
|
17
17
|
import { createListenReplyResolver } from '../../services/listen-reply-resolver.js';
|
|
18
18
|
import { formatReplyContext } from '../../services/reply-context.js';
|
|
19
19
|
import { executeListenReply, parseListenComposerInput } from '../../services/listen-composer-command.js';
|
|
@@ -25,9 +25,12 @@ import { useGroupCommand } from './use-group-command.js';
|
|
|
25
25
|
import { executeGroupCommand } from '../../group-commands/executor.js';
|
|
26
26
|
import { GroupWriteService } from '../../services/group-write-service.js';
|
|
27
27
|
import { ADMIN_RIGHT_KEYS } from '../../services/group-write-service.js';
|
|
28
|
+
import { WriteAccessPolicy } from '../../services/write-access-policy.js';
|
|
28
29
|
import { GroupCommandConfirm } from './group-command-confirm.js';
|
|
29
30
|
import { truncateCell } from './display-width.js';
|
|
31
|
+
import { SecureInput } from './secure-input.js';
|
|
30
32
|
const MESSAGE_SEPARATOR = '────────────────────────────────────────────';
|
|
33
|
+
const OWNERSHIP_SHUTDOWN_GRACE_MS = 250;
|
|
31
34
|
/** Maximum number of grouped messages retained by a long-running interactive listener. */
|
|
32
35
|
export const LISTEN_HISTORY_LIMIT = 500;
|
|
33
36
|
const LISTEN_IMAGE_PREVIEWS_ENABLED = false;
|
|
@@ -60,11 +63,14 @@ export const LISTEN_COMPOSER_THEME = {
|
|
|
60
63
|
target: '#f0d38a',
|
|
61
64
|
hint: '#9bdca8',
|
|
62
65
|
};
|
|
63
|
-
export function
|
|
66
|
+
export function listenComposerCursorColor(visible) {
|
|
67
|
+
return visible ? LISTEN_COMPOSER_THEME.cursor : LISTEN_COMPOSER_THEME.background;
|
|
68
|
+
}
|
|
69
|
+
export function ListenComposer({ input, sendTargetLabel, terminalWidth, sending = false, hint = 'Enter to send · Ctrl+C to exit', cursorVisible = true, }) {
|
|
64
70
|
const inputText = `› ${input}${sending ? ' (sending...)' : ''}`;
|
|
65
71
|
const blankRow = ' '.repeat(Math.max(0, terminalWidth));
|
|
66
72
|
const inputRowFill = ' '.repeat(Math.max(0, terminalWidth - stringWidth(inputText) - 1));
|
|
67
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, children: blankRow }), _jsxs(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, color: LISTEN_COMPOSER_THEME.foreground, children: [inputText, _jsx(Text, { backgroundColor:
|
|
73
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, children: blankRow }), _jsxs(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, color: LISTEN_COMPOSER_THEME.foreground, children: [inputText, _jsx(Text, { backgroundColor: listenComposerCursorColor(cursorVisible), children: " " }), inputRowFill] }), _jsx(Text, { backgroundColor: LISTEN_COMPOSER_THEME.background, children: blankRow }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { color: LISTEN_COMPOSER_THEME.target, children: ["To: ", sendTargetLabel] }), _jsx(Text, { color: LISTEN_COMPOSER_THEME.hint, children: hint })] })] }));
|
|
68
74
|
}
|
|
69
75
|
export function ListenAttachmentLine({ label, selected, state }) {
|
|
70
76
|
const action = state.status === 'idle'
|
|
@@ -196,16 +202,23 @@ export async function runInteractiveAutoDownloadLifecycle(options) {
|
|
|
196
202
|
: null;
|
|
197
203
|
options.onCoordinator?.(coordinator);
|
|
198
204
|
let currentClient = null;
|
|
205
|
+
let closeCurrentClient = null;
|
|
199
206
|
const abort = () => {
|
|
200
207
|
coordinator?.stop();
|
|
201
|
-
void
|
|
208
|
+
void closeCurrentClient?.();
|
|
202
209
|
};
|
|
203
210
|
options.signal.addEventListener('abort', abort);
|
|
204
211
|
try {
|
|
205
212
|
while (!options.signal.aborted) {
|
|
206
213
|
options.onStatus?.('connecting');
|
|
207
214
|
const client = options.createClient();
|
|
215
|
+
let closePromise;
|
|
216
|
+
const closeClient = () => {
|
|
217
|
+
closePromise ??= Promise.resolve().then(() => client.close()).catch(() => undefined);
|
|
218
|
+
return closePromise;
|
|
219
|
+
};
|
|
208
220
|
currentClient = client;
|
|
221
|
+
closeCurrentClient = closeClient;
|
|
209
222
|
options.onClient?.(client);
|
|
210
223
|
coordinator?.setClient(client);
|
|
211
224
|
let retry = false;
|
|
@@ -249,11 +262,13 @@ export async function runInteractiveAutoDownloadLifecycle(options) {
|
|
|
249
262
|
else {
|
|
250
263
|
coordinator?.stop();
|
|
251
264
|
}
|
|
252
|
-
await
|
|
265
|
+
await closeClient();
|
|
253
266
|
if (options.signal.aborted)
|
|
254
267
|
await coordinator?.waitForActive();
|
|
255
268
|
if (currentClient === client)
|
|
256
269
|
currentClient = null;
|
|
270
|
+
if (closeCurrentClient === closeClient)
|
|
271
|
+
closeCurrentClient = null;
|
|
257
272
|
options.onClient?.(null);
|
|
258
273
|
}
|
|
259
274
|
if (!retry)
|
|
@@ -376,8 +391,7 @@ export function calculateListenMessagePaneHeight(terminalHeight, hasNote, autoDo
|
|
|
376
391
|
return Math.max(2, terminalHeight - reservedLines);
|
|
377
392
|
}
|
|
378
393
|
export async function runInteractiveListen(write, run) {
|
|
379
|
-
write
|
|
380
|
-
return run();
|
|
394
|
+
return withAlternateScroll({ write, run });
|
|
381
395
|
}
|
|
382
396
|
export async function renderInteractiveListen(options) {
|
|
383
397
|
await runInteractiveListen(process.stdout.write.bind(process.stdout), async () => {
|
|
@@ -385,7 +399,7 @@ export async function renderInteractiveListen(options) {
|
|
|
385
399
|
await app.waitUntilExit();
|
|
386
400
|
});
|
|
387
401
|
}
|
|
388
|
-
export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo, showMedia, autoDownload, showChatName, createClient, stopSignal, onRequestStop, createReplyResolver, }) {
|
|
402
|
+
export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo, showMedia, autoDownload, showChatName, createClient, stopSignal, onRequestStop, createReplyResolver, shutdownRequests, }) {
|
|
389
403
|
const { exit } = useApp();
|
|
390
404
|
const { stdout } = useStdout();
|
|
391
405
|
const terminalMetrics = useTerminalMetrics(stdout);
|
|
@@ -402,12 +416,12 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
402
416
|
const [knownGroup, setKnownGroup] = useState(undefined);
|
|
403
417
|
const clientRef = useRef(null);
|
|
404
418
|
const replyExecutionLockRef = useRef(false);
|
|
405
|
-
const inputGenerationRef = useRef(
|
|
419
|
+
const inputGenerationRef = useRef({});
|
|
406
420
|
const commandSelectionRef = useRef(0);
|
|
407
421
|
const knownGroupRef = useRef(undefined);
|
|
408
|
-
const groupLookupGenerationRef = useRef(
|
|
422
|
+
const groupLookupGenerationRef = useRef({});
|
|
409
423
|
useEffect(() => {
|
|
410
|
-
groupLookupGenerationRef.current
|
|
424
|
+
groupLookupGenerationRef.current = {};
|
|
411
425
|
knownGroupRef.current = undefined;
|
|
412
426
|
setKnownGroup(undefined);
|
|
413
427
|
}, [sendTo]);
|
|
@@ -418,19 +432,25 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
418
432
|
if (sendTo == null)
|
|
419
433
|
return { ok: false, error: { code: 'ambiguous_chat', message: 'Select exactly one target chat with --send-to.' } };
|
|
420
434
|
let knownGroup = knownGroupRef.current;
|
|
421
|
-
|
|
422
|
-
|
|
435
|
+
const refreshOwnershipCapability = request.key === 'admin transfer-owner'
|
|
436
|
+
&& options?.confirmed === true
|
|
437
|
+
&& options.ownershipPassword == null;
|
|
438
|
+
if (knownGroup == null || refreshOwnershipCapability) {
|
|
439
|
+
const lookup = {};
|
|
440
|
+
groupLookupGenerationRef.current = lookup;
|
|
423
441
|
knownGroup = await client.groups.getGroup(sendTo);
|
|
424
|
-
if (lookup
|
|
425
|
-
|
|
426
|
-
setKnownGroup(knownGroup);
|
|
442
|
+
if (lookup !== groupLookupGenerationRef.current || clientRef.current !== client) {
|
|
443
|
+
return { ok: false, error: { code: 'connection_not_ready', message: 'Telegram connection changed during group verification.' } };
|
|
427
444
|
}
|
|
445
|
+
knownGroupRef.current = knownGroup;
|
|
446
|
+
setKnownGroup(knownGroup);
|
|
428
447
|
}
|
|
429
448
|
return executeGroupCommand(request, {
|
|
430
449
|
chat: sendTo,
|
|
431
450
|
groups: new GroupWriteService(client.groups),
|
|
432
451
|
confirmed: options?.confirmed ?? false,
|
|
433
452
|
confirmationTitle: options?.confirmationTitle,
|
|
453
|
+
ownershipPassword: options?.ownershipPassword,
|
|
434
454
|
knownGroup,
|
|
435
455
|
connectionReady: true,
|
|
436
456
|
targetAvailable: true,
|
|
@@ -440,7 +460,8 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
440
460
|
setKnownGroup(undefined);
|
|
441
461
|
if (request.key === 'chat delete' || request.key === 'chat leave')
|
|
442
462
|
return;
|
|
443
|
-
const lookup =
|
|
463
|
+
const lookup = {};
|
|
464
|
+
groupLookupGenerationRef.current = lookup;
|
|
444
465
|
try {
|
|
445
466
|
const refreshed = await client.groups.getGroup(sendTo);
|
|
446
467
|
if (lookup === groupLookupGenerationRef.current && clientRef.current === client) {
|
|
@@ -455,6 +476,8 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
455
476
|
},
|
|
456
477
|
});
|
|
457
478
|
}, [sendTo]));
|
|
479
|
+
const irreversibleGroupWriteRef = useRef(false);
|
|
480
|
+
irreversibleGroupWriteRef.current = groupCommand.state.kind === 'executing' && groupCommand.state.irreversible === true;
|
|
458
481
|
const terminalWidth = terminalMetrics.columns;
|
|
459
482
|
const terminalHeight = terminalMetrics.rows;
|
|
460
483
|
const previewColorDepth = interactiveListenPreviewColorDepth(terminalMetrics.colorDepth);
|
|
@@ -479,17 +502,52 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
479
502
|
if (operationControllerRef.current == null)
|
|
480
503
|
operationControllerRef.current = createInteractiveOperationController();
|
|
481
504
|
const stoppingRef = useRef(false);
|
|
505
|
+
const lifecycleStopRef = useRef(null);
|
|
506
|
+
const deferredStopRef = useRef(false);
|
|
507
|
+
const shutdownRequestedRef = useRef(false);
|
|
508
|
+
const shutdownGraceTimerRef = useRef(null);
|
|
509
|
+
function requestOwnershipShutdown(source) {
|
|
510
|
+
if (deferredStopRef.current || shutdownRequestedRef.current) {
|
|
511
|
+
deferredStopRef.current = false;
|
|
512
|
+
shutdownRequestedRef.current = false;
|
|
513
|
+
if (shutdownGraceTimerRef.current)
|
|
514
|
+
clearTimeout(shutdownGraceTimerRef.current);
|
|
515
|
+
shutdownGraceTimerRef.current = null;
|
|
516
|
+
groupCommand.setState({ kind: 'error', message: 'Ownership transfer outcome is indeterminate after forced shutdown.' });
|
|
517
|
+
setTimeout(stopListening, 0);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (source === 'terminal') {
|
|
521
|
+
deferredStopRef.current = true;
|
|
522
|
+
setNote('Group write is already running; wait for its outcome.');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
shutdownRequestedRef.current = true;
|
|
526
|
+
setNote('Shutdown requested; waiting briefly for the ownership transfer outcome.');
|
|
527
|
+
shutdownGraceTimerRef.current = setTimeout(() => {
|
|
528
|
+
shutdownGraceTimerRef.current = null;
|
|
529
|
+
groupCommand.setState({
|
|
530
|
+
kind: 'error',
|
|
531
|
+
message: 'Ownership transfer outcome is indeterminate because shutdown grace expired.',
|
|
532
|
+
});
|
|
533
|
+
setTimeout(stopListening, 0);
|
|
534
|
+
}, OWNERSHIP_SHUTDOWN_GRACE_MS);
|
|
535
|
+
}
|
|
482
536
|
const seenRef = useRef(new Set());
|
|
483
537
|
const seenOrderRef = useRef([]);
|
|
484
538
|
const downloadableAttachments = collectDownloadableAttachments(visibleMessages);
|
|
485
539
|
const selectedAttachment = downloadableAttachments[selectedAttachmentIndex] ?? downloadableAttachments[0];
|
|
486
|
-
const { visible: scrollbarVisible } = useTransientScrollbar();
|
|
540
|
+
const { visible: scrollbarVisible, show: showScrollbar } = useTransientScrollbar();
|
|
487
541
|
const scrollbarGeometry = calculateScrollbar({
|
|
488
542
|
height: terminalHeight,
|
|
489
543
|
total: messages.length,
|
|
490
544
|
visible: visibleMessages.length,
|
|
491
545
|
offset: scrollState.offset,
|
|
492
546
|
});
|
|
547
|
+
const scrollMessages = useCallback((direction, amount = 1) => {
|
|
548
|
+
showScrollbar();
|
|
549
|
+
setScrollState((current) => applyScroll(current, direction, Math.max(0, messages.length - 1), amount));
|
|
550
|
+
}, [messages.length, showScrollbar]);
|
|
493
551
|
useEffect(() => {
|
|
494
552
|
const maxOffset = Math.max(0, messages.length - 1);
|
|
495
553
|
setScrollState((current) => {
|
|
@@ -512,11 +570,22 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
512
570
|
useInput((inputText, key) => {
|
|
513
571
|
if (isMouseInput(inputText))
|
|
514
572
|
return;
|
|
573
|
+
const modal = groupCommand.state;
|
|
574
|
+
if (modal.kind === 'executing' && modal.irreversible === true) {
|
|
575
|
+
if (key.ctrl && (inputText === 'c' || inputText === 'C' || inputText === '\u0003')) {
|
|
576
|
+
requestOwnershipShutdown('terminal');
|
|
577
|
+
}
|
|
578
|
+
else if (key.escape) {
|
|
579
|
+
setNote('Group write is already running; wait for its outcome.');
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
515
583
|
if (key.ctrl && (inputText === 'c' || inputText === 'C' || inputText === '\u0003')) {
|
|
516
584
|
stopListening();
|
|
517
585
|
return;
|
|
518
586
|
}
|
|
519
|
-
|
|
587
|
+
if (modal.kind === 'password')
|
|
588
|
+
return;
|
|
520
589
|
if (modal.kind === 'confirm') {
|
|
521
590
|
if (key.escape) {
|
|
522
591
|
closeGroupCommand();
|
|
@@ -556,7 +625,8 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
556
625
|
}
|
|
557
626
|
else {
|
|
558
627
|
const submittedTitle = modal.confirmText;
|
|
559
|
-
const lookup =
|
|
628
|
+
const lookup = {};
|
|
629
|
+
groupLookupGenerationRef.current = lookup;
|
|
560
630
|
void client.groups.getGroup(sendTo).then((fresh) => {
|
|
561
631
|
if (lookup !== groupLookupGenerationRef.current || clientRef.current !== client)
|
|
562
632
|
return;
|
|
@@ -618,12 +688,17 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
618
688
|
closeGroupCommand();
|
|
619
689
|
return;
|
|
620
690
|
}
|
|
691
|
+
if (replyExecutionLockRef.current) {
|
|
692
|
+
if (key.escape)
|
|
693
|
+
setNote('Reply is still sending; wait for its outcome.');
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (key.pageUp || key.pageDown) {
|
|
697
|
+
scrollMessages(key.pageUp ? 'up' : 'down', Math.max(1, visibleMessages.length));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
621
700
|
const slashMode = input.trimStart().startsWith('/');
|
|
622
701
|
if (slashMode && key.escape) {
|
|
623
|
-
if (replyExecutionLockRef.current) {
|
|
624
|
-
inputGenerationRef.current++;
|
|
625
|
-
setSending(false);
|
|
626
|
-
}
|
|
627
702
|
closeGroupCommand();
|
|
628
703
|
return;
|
|
629
704
|
}
|
|
@@ -642,7 +717,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
642
717
|
setNote(failure.error.message);
|
|
643
718
|
return;
|
|
644
719
|
}
|
|
645
|
-
inputGenerationRef.current
|
|
720
|
+
inputGenerationRef.current = {};
|
|
646
721
|
setInput(completeListenCommand(input, selected));
|
|
647
722
|
commandSelectionRef.current = 0;
|
|
648
723
|
groupCommand.setState({ kind: 'menu', selectedIndex: 0 });
|
|
@@ -682,6 +757,10 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
682
757
|
}
|
|
683
758
|
return;
|
|
684
759
|
}
|
|
760
|
+
if (key.upArrow || key.downArrow) {
|
|
761
|
+
scrollMessages(key.upArrow ? 'up' : 'down');
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
685
764
|
if (sending || groupCommand.state.kind === 'executing')
|
|
686
765
|
return;
|
|
687
766
|
if (key.return) {
|
|
@@ -699,7 +778,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
699
778
|
}
|
|
700
779
|
const parsed = parseSelectedListenCommand(input, match);
|
|
701
780
|
if (parsed.kind === 'complete') {
|
|
702
|
-
inputGenerationRef.current
|
|
781
|
+
inputGenerationRef.current = {};
|
|
703
782
|
setInput(parsed.input);
|
|
704
783
|
commandSelectionRef.current = 0;
|
|
705
784
|
groupCommand.setState({ kind: 'menu', selectedIndex: 0 });
|
|
@@ -721,6 +800,11 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
721
800
|
setNote('connection is not ready');
|
|
722
801
|
return;
|
|
723
802
|
}
|
|
803
|
+
const access = new WriteAccessPolicy().check();
|
|
804
|
+
if (!access.ok) {
|
|
805
|
+
setNote(access.error.message);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
724
808
|
replyExecutionLockRef.current = true;
|
|
725
809
|
const ownedGeneration = inputGenerationRef.current;
|
|
726
810
|
const originalInput = input;
|
|
@@ -761,7 +845,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
761
845
|
return;
|
|
762
846
|
}
|
|
763
847
|
if (key.backspace || key.delete) {
|
|
764
|
-
inputGenerationRef.current
|
|
848
|
+
inputGenerationRef.current = {};
|
|
765
849
|
setInput((current) => current.slice(0, -1));
|
|
766
850
|
return;
|
|
767
851
|
}
|
|
@@ -769,7 +853,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
769
853
|
return;
|
|
770
854
|
}
|
|
771
855
|
if (!key.ctrl && !key.meta && inputText.length > 0) {
|
|
772
|
-
inputGenerationRef.current
|
|
856
|
+
inputGenerationRef.current = {};
|
|
773
857
|
setInput((current) => {
|
|
774
858
|
const next = current + inputText;
|
|
775
859
|
if (next.trimStart().startsWith('/')) {
|
|
@@ -784,14 +868,24 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
784
868
|
useEffect(() => {
|
|
785
869
|
const generation = operationControllerRef.current.beginGeneration();
|
|
786
870
|
const isActive = generation.isActive;
|
|
871
|
+
const lifecycleStop = new AbortController();
|
|
872
|
+
lifecycleStopRef.current = lifecycleStop;
|
|
787
873
|
if (stopSignal.aborted) {
|
|
788
874
|
exit();
|
|
789
875
|
return;
|
|
790
876
|
}
|
|
791
877
|
const stopFromSignal = () => {
|
|
878
|
+
if (stoppingRef.current)
|
|
879
|
+
return;
|
|
880
|
+
if (irreversibleGroupWriteRef.current) {
|
|
881
|
+
requestOwnershipShutdown('external');
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
792
884
|
stopListening();
|
|
793
885
|
};
|
|
794
|
-
|
|
886
|
+
const unsubscribeShutdown = shutdownRequests?.subscribe(stopFromSignal);
|
|
887
|
+
if (shutdownRequests == null)
|
|
888
|
+
stopSignal.addEventListener('abort', stopFromSignal);
|
|
795
889
|
const groupQueue = createInteractiveListenRuntime(dbPath, createReplyResolver ?? createListenReplyResolver, {
|
|
796
890
|
isActive,
|
|
797
891
|
onGroup: (group) => {
|
|
@@ -833,7 +927,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
833
927
|
chats,
|
|
834
928
|
persist,
|
|
835
929
|
retrySeconds,
|
|
836
|
-
signal:
|
|
930
|
+
signal: lifecycleStop.signal,
|
|
837
931
|
createClient,
|
|
838
932
|
createCoordinator: () => autoDownloader,
|
|
839
933
|
onCoordinator: (coordinator) => {
|
|
@@ -843,12 +937,13 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
843
937
|
onClient: (client) => {
|
|
844
938
|
if (!isActive())
|
|
845
939
|
return;
|
|
846
|
-
groupLookupGenerationRef.current
|
|
940
|
+
groupLookupGenerationRef.current = {};
|
|
847
941
|
knownGroupRef.current = undefined;
|
|
848
942
|
setKnownGroup(undefined);
|
|
849
943
|
clientRef.current = client;
|
|
850
944
|
if (client != null && sendTo != null) {
|
|
851
|
-
const lookup =
|
|
945
|
+
const lookup = {};
|
|
946
|
+
groupLookupGenerationRef.current = lookup;
|
|
852
947
|
void client.groups.getGroup(sendTo).then((group) => {
|
|
853
948
|
if (isActive() && lookup === groupLookupGenerationRef.current && clientRef.current === client) {
|
|
854
949
|
knownGroupRef.current = group;
|
|
@@ -903,30 +998,42 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
903
998
|
albumAggregator.flush();
|
|
904
999
|
},
|
|
905
1000
|
}).finally(() => {
|
|
906
|
-
if (isActive() && !stopSignal.aborted) {
|
|
1001
|
+
if (isActive() && !stopSignal.aborted && !stoppingRef.current) {
|
|
907
1002
|
onRequestStop();
|
|
908
1003
|
exit();
|
|
909
1004
|
}
|
|
910
1005
|
});
|
|
911
1006
|
return () => {
|
|
912
|
-
groupLookupGenerationRef.current
|
|
1007
|
+
groupLookupGenerationRef.current = {};
|
|
913
1008
|
knownGroupRef.current = undefined;
|
|
914
|
-
|
|
1009
|
+
unsubscribeShutdown?.();
|
|
1010
|
+
if (shutdownRequests == null)
|
|
1011
|
+
stopSignal.removeEventListener('abort', stopFromSignal);
|
|
915
1012
|
albumAggregator.flush();
|
|
916
1013
|
generation.dispose();
|
|
917
1014
|
albumAggregator.dispose();
|
|
918
1015
|
void groupQueue.close();
|
|
919
1016
|
if (albumAggregatorRef.current === albumAggregator)
|
|
920
1017
|
albumAggregatorRef.current = null;
|
|
921
|
-
|
|
1018
|
+
lifecycleStop.abort();
|
|
922
1019
|
clientRef.current = null;
|
|
923
1020
|
autoDownloader?.stop();
|
|
924
1021
|
void autoDownloader?.waitForActive();
|
|
925
1022
|
if (autoDownloaderRef.current === autoDownloader)
|
|
926
1023
|
autoDownloaderRef.current = null;
|
|
1024
|
+
if (lifecycleStopRef.current === lifecycleStop)
|
|
1025
|
+
lifecycleStopRef.current = null;
|
|
1026
|
+
if (shutdownGraceTimerRef.current)
|
|
1027
|
+
clearTimeout(shutdownGraceTimerRef.current);
|
|
1028
|
+
shutdownGraceTimerRef.current = null;
|
|
927
1029
|
};
|
|
928
|
-
}, [autoDownload, chats, createClient, createReplyResolver, dbPath, persist, retrySeconds, sendTo, showMedia, exit, stopSignal, onRequestStop]);
|
|
1030
|
+
}, [autoDownload, chats, createClient, createReplyResolver, dbPath, persist, retrySeconds, sendTo, showMedia, exit, stopSignal, shutdownRequests, onRequestStop]);
|
|
929
1031
|
const sendMessage = async (text) => {
|
|
1032
|
+
const access = new WriteAccessPolicy().check();
|
|
1033
|
+
if (!access.ok) {
|
|
1034
|
+
setNote(access.error.message);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
930
1037
|
const trimmed = text.trim();
|
|
931
1038
|
if (!trimmed)
|
|
932
1039
|
return;
|
|
@@ -997,7 +1104,7 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
997
1104
|
onProgress: (downloaded, total) => {
|
|
998
1105
|
if (!isCurrent())
|
|
999
1106
|
return;
|
|
1000
|
-
const progress =
|
|
1107
|
+
const progress = attachmentDownloadProgress(downloaded, total);
|
|
1001
1108
|
setDownloadStates((current) => ({ ...current, [item.key]: { status: 'downloading', progress } }));
|
|
1002
1109
|
},
|
|
1003
1110
|
});
|
|
@@ -1019,15 +1126,28 @@ export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo
|
|
|
1019
1126
|
setTimeout(exit, 0);
|
|
1020
1127
|
onRequestStop();
|
|
1021
1128
|
autoDownloaderRef.current?.stop();
|
|
1022
|
-
|
|
1129
|
+
lifecycleStopRef.current?.abort();
|
|
1023
1130
|
};
|
|
1131
|
+
useEffect(() => {
|
|
1132
|
+
if (groupCommand.state.kind !== 'result' && groupCommand.state.kind !== 'error')
|
|
1133
|
+
return;
|
|
1134
|
+
if (!deferredStopRef.current && !shutdownRequestedRef.current)
|
|
1135
|
+
return;
|
|
1136
|
+
deferredStopRef.current = false;
|
|
1137
|
+
shutdownRequestedRef.current = false;
|
|
1138
|
+
if (shutdownGraceTimerRef.current)
|
|
1139
|
+
clearTimeout(shutdownGraceTimerRef.current);
|
|
1140
|
+
shutdownGraceTimerRef.current = null;
|
|
1141
|
+
const pending = setTimeout(stopListening, 0);
|
|
1142
|
+
return () => clearTimeout(pending);
|
|
1143
|
+
}, [groupCommand.state]);
|
|
1024
1144
|
const permissionState = groupCommand.state.kind === 'select-permissions' ? groupCommand.state : null;
|
|
1025
1145
|
return (_jsxs(Box, { flexDirection: "row", width: terminalWidth, height: terminalHeight, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: contentWidth, height: terminalHeight, overflow: "hidden", children: [_jsx(ListenStatusArea, { status: status, unseenCount: scrollState.unseenCount, autoDownload: autoDownload }), note ? _jsx(Text, { dimColor: true, children: note }) : null, groupCommand.state.kind === 'result' ? _jsx(Box, { flexGrow: 1, flexDirection: "column", children: _jsx(GroupCommandResult, { state: groupCommand.state, width: contentWidth }) }) : _jsxs(Box, { flexGrow: 1, flexDirection: "column", children: [_jsx(GroupCommandResult, { state: groupCommand.state, width: contentWidth }), groupCommand.state.kind === 'confirm' && 'confirmation' in groupCommand.state.pending ? _jsx(GroupCommandConfirm, { confirmation: groupCommand.state.pending.confirmation, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth }) : null, groupCommand.state.kind === 'confirm-title' && 'confirmation' in groupCommand.state.pending ? groupCommand.state.stage === 'confirm'
|
|
1026
1146
|
? _jsx(GroupCommandConfirm, { confirmation: groupCommand.state.pending.confirmation, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth })
|
|
1027
|
-
: _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "yellow", children: truncateCell('Type the exact title to permanently delete this chat:', contentWidth) }), _jsx(Text, { children: truncateCell(groupCommand.state.pending.confirmation.title ?? '', contentWidth) }), _jsx(Text, { color: "#8ecbff", children: truncateCell(`› ${groupCommand.state.confirmText}`, contentWidth) }), groupCommand.state.mismatch ? _jsx(Text, { color: "red", children: truncateCell('Title does not match exactly.', contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('Enter verify · Esc back', contentWidth) })] }) : null, permissionState ? _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "#8ecbff", children: truncateCell('Administrator permissions', contentWidth) }), ADMIN_RIGHT_KEYS.map((right, index) => _jsx(Text, { color: index === permissionState.selectedIndex ? '#8ecbff' : undefined, children: truncateCell(`${index === permissionState.selectedIndex ? '› ' : ' '}[${permissionState.selected.includes(right) ? 'x' : ' '}] ${right}`, contentWidth) }, right)), permissionState.warning ? _jsx(Text, { color: "yellow", children: truncateCell(permissionState.warning, contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('↑/↓ select · Space toggle · Enter continue · Esc cancel', contentWidth) })] }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [messages.length === 0 ? _jsx(Text, { dimColor: true, children: "Waiting for new messages..." }) : null, visibleMessages.map((message) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: formatInteractiveListenHeader(message) }), message.replyContext == null ? null : _jsx(Text, { wrap: "truncate-end", children: formatReplyContext(message.replyContext) }), message.content == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.content }), message.mediaSummary == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.mediaSummary }), message.media.map((item, mediaIndex) => {
|
|
1147
|
+
: _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "yellow", children: truncateCell('Type the exact title to permanently delete this chat:', contentWidth) }), _jsx(Text, { children: truncateCell(groupCommand.state.pending.confirmation.title ?? '', contentWidth) }), _jsx(Text, { color: "#8ecbff", children: truncateCell(`› ${groupCommand.state.confirmText}`, contentWidth) }), groupCommand.state.mismatch ? _jsx(Text, { color: "red", children: truncateCell('Title does not match exactly.', contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('Enter verify · Esc back', contentWidth) })] }) : null, groupCommand.state.kind === 'password' ? _jsx(SecureInput, { label: "Telegram 2FA password", onSubmit: (value) => { void groupCommand.runWithOwnershipPassword(value); }, onCancel: closeGroupCommand }) : null, permissionState ? _jsxs(Box, { flexDirection: "column", width: contentWidth, children: [_jsx(Text, { color: "#8ecbff", children: truncateCell('Administrator permissions', contentWidth) }), ADMIN_RIGHT_KEYS.map((right, index) => _jsx(Text, { color: index === permissionState.selectedIndex ? '#8ecbff' : undefined, children: truncateCell(`${index === permissionState.selectedIndex ? '› ' : ' '}[${permissionState.selected.includes(right) ? 'x' : ' '}] ${right}`, contentWidth) }, right)), permissionState.warning ? _jsx(Text, { color: "yellow", children: truncateCell(permissionState.warning, contentWidth) }) : null, _jsx(Text, { dimColor: true, children: truncateCell('↑/↓ select · Space toggle · Enter continue · Esc cancel', contentWidth) })] }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [messages.length === 0 ? _jsx(Text, { dimColor: true, children: "Waiting for new messages..." }) : null, visibleMessages.map((message) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, wrap: "truncate-end", children: formatInteractiveListenHeader(message) }), message.replyContext == null ? null : _jsx(Text, { wrap: "truncate-end", children: formatReplyContext(message.replyContext) }), message.content == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.content }), message.mediaSummary == null ? null : _jsx(Text, { wrap: "truncate-end", children: message.mediaSummary }), message.media.map((item, mediaIndex) => {
|
|
1028
1148
|
const attachmentKey = attachmentDownloadKeyAt(message.media, mediaIndex);
|
|
1029
1149
|
return (_jsx(ListenAttachmentWithPreview, { label: item.label, downloadable: item.downloadable, selected: focus === 'attachments' && selectedAttachment?.key === attachmentKey, state: downloadStates[attachmentKey] ?? { status: 'idle' }, previewCells: item.previewCells }, attachmentKey));
|
|
1030
|
-
}), _jsx(Text, { dimColor: true, children: MESSAGE_SEPARATOR })] }, message.key)))] })] }), groupCommand.state.kind !== 'result' ? _jsxs(Box, { flexDirection: "column", children: [sendTo == null ? _jsx(Text, { dimColor: true, children: "Set --send-to <chat> (or pass one chat to listen) before sending messages." }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [input.trimStart().startsWith('/') && groupCommand.state.kind === 'menu' ? _jsx(ListenCommandMenu, { input: input, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth, knownGroup: knownGroup }) : null, _jsx(ListenComposer, { input: input, sendTargetLabel: sendTo == null ? '(not selected)' : sendTargetLabel, terminalWidth: contentWidth, sending: sending, hint: focus === 'attachments' ? '↑/↓ select · Enter download · Tab input' : '
|
|
1150
|
+
}), _jsx(Text, { dimColor: true, children: MESSAGE_SEPARATOR })] }, message.key)))] })] }), groupCommand.state.kind !== 'result' ? _jsxs(Box, { flexDirection: "column", children: [sendTo == null ? _jsx(Text, { dimColor: true, children: "Set --send-to <chat> (or pass one chat to listen) before sending messages." }) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [input.trimStart().startsWith('/') && groupCommand.state.kind === 'menu' ? _jsx(ListenCommandMenu, { input: input, selectedIndex: groupCommand.state.selectedIndex, width: contentWidth, knownGroup: knownGroup }) : null, _jsx(ListenComposer, { input: input, sendTargetLabel: sendTo == null ? '(not selected)' : sendTargetLabel, terminalWidth: contentWidth, sending: sending, cursorVisible: focus === 'input', hint: focus === 'attachments' ? '↑/↓ select · Enter download · Tab input' : 'Wheel/↑/↓/PgUp/PgDn scroll · Drag select · Ctrl+C exit' })] })] }) : null] }), _jsx(ListenScrollbar, { height: terminalHeight, visible: scrollbarVisible, geometry: scrollbarGeometry })] }));
|
|
1031
1151
|
}
|
|
1032
1152
|
export class ListenMessageViewCache {
|
|
1033
1153
|
entries = new Map();
|