@will-17173/telegram-cli 0.1.1 → 0.3.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 +169 -10
- package/README.zh-CN.md +169 -11
- package/dist/cli/app.js +3 -1
- package/dist/cli/app.js.map +1 -1
- package/dist/commands/account.js +51 -5
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/config.js +96 -13
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/group-write.js +88 -0
- package/dist/commands/group-write.js.map +1 -0
- package/dist/commands/group.js +121 -0
- package/dist/commands/group.js.map +1 -0
- package/dist/commands/telegram-runner.js +84 -0
- package/dist/commands/telegram-runner.js.map +1 -0
- package/dist/commands/telegram.js +154 -172
- package/dist/commands/telegram.js.map +1 -1
- package/dist/config/credential-store.js +76 -22
- package/dist/config/credential-store.js.map +1 -1
- package/dist/config/env.js +12 -1
- package/dist/config/env.js.map +1 -1
- package/dist/group-commands/catalog.js +69 -0
- package/dist/group-commands/catalog.js.map +1 -0
- package/dist/group-commands/executor.js +93 -0
- package/dist/group-commands/executor.js.map +1 -0
- package/dist/group-commands/parser.js +268 -0
- package/dist/group-commands/parser.js.map +1 -0
- package/dist/group-commands/tokenize.js +51 -0
- package/dist/group-commands/tokenize.js.map +1 -0
- package/dist/group-commands/types.js +2 -0
- package/dist/group-commands/types.js.map +1 -0
- package/dist/listen-commands/catalog.js +61 -0
- package/dist/listen-commands/catalog.js.map +1 -0
- package/dist/listen-commands/dispatch.js +46 -0
- package/dist/listen-commands/dispatch.js.map +1 -0
- package/dist/listen-commands/match.js +113 -0
- package/dist/listen-commands/match.js.map +1 -0
- package/dist/presenters/group.js +130 -0
- package/dist/presenters/group.js.map +1 -0
- package/dist/presenters/human.js +29 -9
- package/dist/presenters/human.js.map +1 -1
- package/dist/presenters/ink/display-width.js +45 -7
- package/dist/presenters/ink/display-width.js.map +1 -1
- package/dist/presenters/ink/group-command-confirm.js +13 -0
- package/dist/presenters/ink/group-command-confirm.js.map +1 -0
- package/dist/presenters/ink/group-command-result.js +21 -0
- package/dist/presenters/ink/group-command-result.js.map +1 -0
- package/dist/presenters/ink/listen-command-menu.js +38 -0
- package/dist/presenters/ink/listen-command-menu.js.map +1 -0
- package/dist/presenters/ink/listen-scroll.js +5 -1
- package/dist/presenters/ink/listen-scroll.js.map +1 -1
- package/dist/presenters/ink/listen.js +806 -171
- package/dist/presenters/ink/listen.js.map +1 -1
- package/dist/presenters/ink/use-group-command.js +125 -0
- package/dist/presenters/ink/use-group-command.js.map +1 -0
- package/dist/presenters/listen-message.js +25 -241
- package/dist/presenters/listen-message.js.map +1 -1
- package/dist/presenters/logical-message.js +78 -0
- package/dist/presenters/logical-message.js.map +1 -0
- package/dist/services/attachment-download.js +5 -2
- package/dist/services/attachment-download.js.map +1 -1
- package/dist/services/auto-download-coordinator.js +214 -0
- package/dist/services/auto-download-coordinator.js.map +1 -0
- package/dist/services/group-service.js +164 -0
- package/dist/services/group-service.js.map +1 -0
- package/dist/services/group-write-service.js +165 -0
- package/dist/services/group-write-service.js.map +1 -0
- package/dist/services/listen-album-aggregator.js +9 -29
- package/dist/services/listen-album-aggregator.js.map +1 -1
- package/dist/services/listen-attachment.js +265 -0
- package/dist/services/listen-attachment.js.map +1 -0
- package/dist/services/listen-composer-command.js +102 -0
- package/dist/services/listen-composer-command.js.map +1 -0
- package/dist/services/listen-reply-resolver.js +126 -0
- package/dist/services/listen-reply-resolver.js.map +1 -0
- package/dist/services/message-service.js +54 -4
- package/dist/services/message-service.js.map +1 -1
- package/dist/services/query-service.js +95 -34
- package/dist/services/query-service.js.map +1 -1
- package/dist/services/reply-context.js +29 -0
- package/dist/services/reply-context.js.map +1 -0
- package/dist/services/sync-service.js +18 -6
- package/dist/services/sync-service.js.map +1 -1
- package/dist/storage/message-db.js +241 -5
- package/dist/storage/message-db.js.map +1 -1
- package/dist/telegram/client-factory.js +4 -2
- package/dist/telegram/client-factory.js.map +1 -1
- package/dist/telegram/fake-group-management.js +381 -0
- package/dist/telegram/fake-group-management.js.map +1 -0
- package/dist/telegram/group-types.js +59 -0
- package/dist/telegram/group-types.js.map +1 -0
- package/dist/telegram/group-write-types.js +2 -0
- package/dist/telegram/group-write-types.js.map +1 -0
- package/dist/telegram/mtcute-client.js +74 -9
- package/dist/telegram/mtcute-client.js.map +1 -1
- package/dist/telegram/mtcute-group-helpers.js +56 -0
- package/dist/telegram/mtcute-group-helpers.js.map +1 -0
- package/dist/telegram/mtcute-group-invites.js +82 -0
- package/dist/telegram/mtcute-group-invites.js.map +1 -0
- package/dist/telegram/mtcute-group-management.js +493 -0
- package/dist/telegram/mtcute-group-management.js.map +1 -0
- package/dist/telegram/mtcute-group-members.js +118 -0
- package/dist/telegram/mtcute-group-members.js.map +1 -0
- package/dist/telegram/mtcute-group-settings.js +146 -0
- package/dist/telegram/mtcute-group-settings.js.map +1 -0
- package/dist/telegram/mtcute-group-topics.js +63 -0
- package/dist/telegram/mtcute-group-topics.js.map +1 -0
- package/dist/telegram/proxy.js +27 -0
- package/dist/telegram/proxy.js.map +1 -0
- package/dist/telegram/raw-message.js +46 -0
- package/dist/telegram/raw-message.js.map +1 -0
- package/package.json +9 -1
|
@@ -7,11 +7,26 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { dirname } from 'node:path';
|
|
8
8
|
import { resolveAttachmentDestination } from '../../services/attachment-download.js';
|
|
9
9
|
import { ListenAlbumAggregator } from '../../services/listen-album-aggregator.js';
|
|
10
|
+
import { AutoDownloadCoordinator } from '../../services/auto-download-coordinator.js';
|
|
11
|
+
import { attachmentDownloadTarget, attachmentFileName, discoverListenAttachments, listenAttachmentKey } from '../../services/listen-attachment.js';
|
|
10
12
|
import { buildListenMessage } from '../listen-message.js';
|
|
11
|
-
import { applyMessageArrival,
|
|
13
|
+
import { applyMessageArrival, takeListenViewport } from './listen-scroll.js';
|
|
12
14
|
import { decodeImagePreview } from './image-preview.js';
|
|
13
15
|
import { ListenScrollbar, calculateScrollbar, listenContentWidth, useTransientScrollbar } from './listen-scrollbar.js';
|
|
14
|
-
import {
|
|
16
|
+
import { DISABLE_MOUSE_REPORTING, isMouseInput } from './mouse-scroll.js';
|
|
17
|
+
import { createListenReplyResolver } from '../../services/listen-reply-resolver.js';
|
|
18
|
+
import { formatReplyContext } from '../../services/reply-context.js';
|
|
19
|
+
import { executeListenReply, parseListenComposerInput } from '../../services/listen-composer-command.js';
|
|
20
|
+
import { ListenCommandMenu, listenCommandMenuAvailability, moveListenCommandSelectionEnabled } from './listen-command-menu.js';
|
|
21
|
+
import { completeListenCommand, visibleListenCommandMatches } from '../../listen-commands/match.js';
|
|
22
|
+
import { parseSelectedListenCommand } from '../../listen-commands/dispatch.js';
|
|
23
|
+
import { GroupCommandResult } from './group-command-result.js';
|
|
24
|
+
import { useGroupCommand } from './use-group-command.js';
|
|
25
|
+
import { executeGroupCommand } from '../../group-commands/executor.js';
|
|
26
|
+
import { GroupWriteService } from '../../services/group-write-service.js';
|
|
27
|
+
import { ADMIN_RIGHT_KEYS } from '../../services/group-write-service.js';
|
|
28
|
+
import { GroupCommandConfirm } from './group-command-confirm.js';
|
|
29
|
+
import { truncateCell } from './display-width.js';
|
|
15
30
|
const MESSAGE_SEPARATOR = '────────────────────────────────────────────';
|
|
16
31
|
/** Maximum number of grouped messages retained by a long-running interactive listener. */
|
|
17
32
|
export const LISTEN_HISTORY_LIMIT = 500;
|
|
@@ -54,24 +69,315 @@ export function ListenComposer({ input, sendTargetLabel, terminalWidth, sending
|
|
|
54
69
|
export function ListenAttachmentLine({ label, selected, state }) {
|
|
55
70
|
const action = state.status === 'idle'
|
|
56
71
|
? ' Download'
|
|
57
|
-
: state.status === '
|
|
58
|
-
?
|
|
59
|
-
: state.status === '
|
|
60
|
-
? state.
|
|
61
|
-
:
|
|
72
|
+
: state.status === 'queued'
|
|
73
|
+
? 'Queued'
|
|
74
|
+
: state.status === 'downloading'
|
|
75
|
+
? `Downloading${state.progress == null ? '...' : ` ${state.progress}%`}`
|
|
76
|
+
: state.status === 'completed'
|
|
77
|
+
? state.path
|
|
78
|
+
: `Failed: ${state.error}`;
|
|
62
79
|
return (_jsxs(Text, { wrap: "truncate-end", backgroundColor: selected ? '#3b5368' : undefined, color: selected ? 'white' : undefined, children: [selected ? '› ' : ' ', label, " [", action, "]"] }));
|
|
63
80
|
}
|
|
81
|
+
export function applyAutoDownloadEvent(current, event, showMedia = true) {
|
|
82
|
+
if (!showMedia)
|
|
83
|
+
return Object.keys(current).length === 0 ? current : {};
|
|
84
|
+
if (event.status === 'cancelled') {
|
|
85
|
+
if (!(event.key in current))
|
|
86
|
+
return current;
|
|
87
|
+
const next = { ...current };
|
|
88
|
+
delete next[event.key];
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
const state = event.status === 'queued'
|
|
92
|
+
? { status: 'queued' }
|
|
93
|
+
: event.status === 'downloading'
|
|
94
|
+
? { status: 'downloading', progress: event.progress }
|
|
95
|
+
: event.status === 'completed'
|
|
96
|
+
? { status: 'completed', path: event.path }
|
|
97
|
+
: { status: 'failed', error: event.error };
|
|
98
|
+
return { ...current, [event.key]: state };
|
|
99
|
+
}
|
|
100
|
+
export function canManuallyDownload(state) {
|
|
101
|
+
return state.status === 'idle' || state.status === 'failed';
|
|
102
|
+
}
|
|
103
|
+
export function createInteractiveOperationController() {
|
|
104
|
+
let generation = 0;
|
|
105
|
+
let sequence = 0;
|
|
106
|
+
let sendOperation = 0;
|
|
107
|
+
const downloadOperations = new Map();
|
|
108
|
+
return {
|
|
109
|
+
beginGeneration() {
|
|
110
|
+
const ownedGeneration = ++generation;
|
|
111
|
+
let active = true;
|
|
112
|
+
return {
|
|
113
|
+
isActive: () => active && generation === ownedGeneration,
|
|
114
|
+
dispose: () => {
|
|
115
|
+
active = false;
|
|
116
|
+
if (generation === ownedGeneration) {
|
|
117
|
+
generation += 1;
|
|
118
|
+
downloadOperations.clear();
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
beginSend() {
|
|
124
|
+
const ownedGeneration = generation;
|
|
125
|
+
const operation = ++sendOperation;
|
|
126
|
+
return () => generation === ownedGeneration && sendOperation === operation;
|
|
127
|
+
},
|
|
128
|
+
beginDownload(key) {
|
|
129
|
+
const ownedGeneration = generation;
|
|
130
|
+
const operation = ++sequence;
|
|
131
|
+
downloadOperations.set(key, operation);
|
|
132
|
+
return {
|
|
133
|
+
isCurrent: () => generation === ownedGeneration && downloadOperations.get(key) === operation,
|
|
134
|
+
release: () => {
|
|
135
|
+
if (downloadOperations.get(key) === operation)
|
|
136
|
+
downloadOperations.delete(key);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
claimDownload(key) {
|
|
141
|
+
const operation = ++sequence;
|
|
142
|
+
downloadOperations.set(key, operation);
|
|
143
|
+
return {
|
|
144
|
+
release: () => {
|
|
145
|
+
if (downloadOperations.get(key) === operation)
|
|
146
|
+
downloadOperations.delete(key);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
downloadOwnershipSize: () => downloadOperations.size,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export async function runOwnedAttachmentOperation(ownership, operation, onError) {
|
|
154
|
+
try {
|
|
155
|
+
await operation();
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (ownership.isCurrent())
|
|
159
|
+
onError(error);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
ownership.release();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function pruneAttachmentDownloadStates(current, validKeys, pendingKeys = new Set()) {
|
|
166
|
+
const retained = Object.fromEntries(Object.entries(current).filter(([key, state]) => (validKeys.has(key) || pendingKeys.has(key) || state.status === 'queued' || state.status === 'downloading')));
|
|
167
|
+
return Object.keys(retained).length === Object.keys(current).length ? current : retained;
|
|
168
|
+
}
|
|
169
|
+
export function registerPendingAttachmentKeys(pendingKeys, message, showMedia) {
|
|
170
|
+
if (!showMedia) {
|
|
171
|
+
pendingKeys.clear();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
discoverListenAttachments(message).forEach((attachment, index) => {
|
|
175
|
+
if (attachment.downloadable)
|
|
176
|
+
pendingKeys.add(listenAttachmentKey(attachment, index));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export function acceptListenMessage(message, seen, seenOrder, emit) {
|
|
180
|
+
const key = `${message.chat_id}:${message.msg_id}`;
|
|
181
|
+
if (seen.has(key))
|
|
182
|
+
return false;
|
|
183
|
+
seen.add(key);
|
|
184
|
+
seenOrder.push(key);
|
|
185
|
+
if (seen.size > 5000) {
|
|
186
|
+
const oldest = seenOrder.shift();
|
|
187
|
+
if (oldest != null)
|
|
188
|
+
seen.delete(oldest);
|
|
189
|
+
}
|
|
190
|
+
emit(message);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
export async function runInteractiveAutoDownloadLifecycle(options) {
|
|
194
|
+
const coordinator = options.autoDownload
|
|
195
|
+
? (options.createCoordinator ?? (() => new AutoDownloadCoordinator()))()
|
|
196
|
+
: null;
|
|
197
|
+
options.onCoordinator?.(coordinator);
|
|
198
|
+
let currentClient = null;
|
|
199
|
+
const abort = () => {
|
|
200
|
+
coordinator?.stop();
|
|
201
|
+
void currentClient?.close().catch(() => undefined);
|
|
202
|
+
};
|
|
203
|
+
options.signal.addEventListener('abort', abort);
|
|
204
|
+
try {
|
|
205
|
+
while (!options.signal.aborted) {
|
|
206
|
+
options.onStatus?.('connecting');
|
|
207
|
+
const client = options.createClient();
|
|
208
|
+
currentClient = client;
|
|
209
|
+
options.onClient?.(client);
|
|
210
|
+
coordinator?.setClient(client);
|
|
211
|
+
let retry = false;
|
|
212
|
+
try {
|
|
213
|
+
const result = await client.listen({
|
|
214
|
+
chats: options.chats,
|
|
215
|
+
signal: options.signal,
|
|
216
|
+
onConnected: () => options.onStatus?.('connected'),
|
|
217
|
+
onMessage: (message) => {
|
|
218
|
+
if (options.signal.aborted)
|
|
219
|
+
return;
|
|
220
|
+
if (options.acceptMessage?.(message) === false)
|
|
221
|
+
return;
|
|
222
|
+
options.onBeforeEnqueue?.(message);
|
|
223
|
+
coordinator?.enqueue(message);
|
|
224
|
+
options.onMessage(message);
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
if (options.persist && result === 'disconnected') {
|
|
228
|
+
retry = true;
|
|
229
|
+
options.onStatus?.('disconnected');
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
options.onStatus?.('stopped');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (options.persist && !options.signal.aborted)
|
|
237
|
+
retry = true;
|
|
238
|
+
options.onError?.(error);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
options.flush?.();
|
|
242
|
+
if (retry) {
|
|
243
|
+
coordinator?.setClient(null);
|
|
244
|
+
await coordinator?.waitForActive();
|
|
245
|
+
}
|
|
246
|
+
else if (!options.signal.aborted) {
|
|
247
|
+
await coordinator?.waitForIdle();
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
coordinator?.stop();
|
|
251
|
+
}
|
|
252
|
+
await client.close().catch(() => undefined);
|
|
253
|
+
if (options.signal.aborted)
|
|
254
|
+
await coordinator?.waitForActive();
|
|
255
|
+
if (currentClient === client)
|
|
256
|
+
currentClient = null;
|
|
257
|
+
options.onClient?.(null);
|
|
258
|
+
}
|
|
259
|
+
if (!retry)
|
|
260
|
+
break;
|
|
261
|
+
await (options.sleep ?? sleep)(options.retrySeconds);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
options.signal.removeEventListener('abort', abort);
|
|
266
|
+
options.onCoordinator?.(null);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export function attachmentDownloadKeyAt(attachments, index) {
|
|
270
|
+
const attachment = attachments[index];
|
|
271
|
+
if (attachment == null)
|
|
272
|
+
throw new Error(`Missing attachment at index ${index}`);
|
|
273
|
+
const sourceIndex = attachments.slice(0, index).filter((candidate) => (candidate.chatId === attachment.chatId && candidate.messageId === attachment.messageId)).length;
|
|
274
|
+
return listenAttachmentKey(attachment, sourceIndex);
|
|
275
|
+
}
|
|
64
276
|
export function ListenImagePreview({ rows }) {
|
|
65
277
|
return (_jsx(Box, { flexDirection: "column", children: rows.map((row, rowIndex) => (_jsxs(Text, { children: [" ", row.map((cell, cellIndex) => (_jsx(Text, { color: cell.foreground, backgroundColor: cell.background, children: cell.glyph }, cellIndex)))] }, rowIndex))) }));
|
|
66
278
|
}
|
|
67
|
-
export function ListenAttachmentWithPreview({ label, selected, state, previewCells, }) {
|
|
68
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
279
|
+
export function ListenAttachmentWithPreview({ label, downloadable = true, selected, state, previewCells, }) {
|
|
280
|
+
return (_jsxs(Box, { flexDirection: "column", children: [downloadable
|
|
281
|
+
? _jsx(ListenAttachmentLine, { label: label, selected: selected, state: state })
|
|
282
|
+
: _jsxs(Text, { wrap: "truncate-end", children: [" ", label] }), previewCells == null ? null : _jsx(ListenImagePreview, { rows: previewCells })] }));
|
|
283
|
+
}
|
|
284
|
+
export function ListenMessageBody({ message }) {
|
|
285
|
+
return (_jsxs(Box, { flexDirection: "column", children: [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) => (_jsx(ListenAttachmentWithPreview, { label: item.label, downloadable: item.downloadable, selected: false, state: { status: 'idle' }, previewCells: item.previewCells }, attachmentDownloadKeyAt(message.media, mediaIndex))))] }));
|
|
286
|
+
}
|
|
287
|
+
export function createInteractiveListenGroupQueue(options) {
|
|
288
|
+
const pending = [];
|
|
289
|
+
const schedule = options.schedule ?? ((run) => setImmediate(run));
|
|
290
|
+
let scheduled = null;
|
|
291
|
+
let resolveScheduled = null;
|
|
292
|
+
let closing = false;
|
|
293
|
+
let closed = false;
|
|
294
|
+
let closePromise = null;
|
|
295
|
+
const closeResolver = async () => {
|
|
296
|
+
if (closed)
|
|
297
|
+
return;
|
|
298
|
+
closed = true;
|
|
299
|
+
if (options.resolver.closeAsync != null)
|
|
300
|
+
await options.resolver.closeAsync();
|
|
301
|
+
else
|
|
302
|
+
options.resolver.close();
|
|
303
|
+
};
|
|
304
|
+
const drain = async () => {
|
|
305
|
+
try {
|
|
306
|
+
while (pending.length > 0) {
|
|
307
|
+
const messages = pending.shift();
|
|
308
|
+
try {
|
|
309
|
+
const replyContext = options.resolver.resolveAsync == null
|
|
310
|
+
? options.resolver.resolve(messages)
|
|
311
|
+
: await options.resolver.resolveAsync(messages);
|
|
312
|
+
options.resolver.remember(messages);
|
|
313
|
+
const first = messages[0];
|
|
314
|
+
if (first != null && options.isActive()) {
|
|
315
|
+
options.onGroup({ key: `${first.chat_id}:${first.msg_id}`, messages, replyContext });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
options.onError(error);
|
|
320
|
+
pending.length = 0;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
resolveScheduled?.();
|
|
327
|
+
resolveScheduled = null;
|
|
328
|
+
scheduled = null;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
return {
|
|
332
|
+
enqueue(messages) {
|
|
333
|
+
if (closing)
|
|
334
|
+
return;
|
|
335
|
+
pending.push(messages);
|
|
336
|
+
if (scheduled != null)
|
|
337
|
+
return;
|
|
338
|
+
scheduled = new Promise((resolve) => { resolveScheduled = resolve; });
|
|
339
|
+
schedule(() => { void drain(); });
|
|
340
|
+
},
|
|
341
|
+
close() {
|
|
342
|
+
closePromise ??= (async () => {
|
|
343
|
+
closing = true;
|
|
344
|
+
const drainCompletion = scheduled;
|
|
345
|
+
if (drainCompletion != null)
|
|
346
|
+
await drainCompletion;
|
|
347
|
+
await closeResolver();
|
|
348
|
+
})();
|
|
349
|
+
return closePromise;
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export function createInteractiveListenRuntime(dbPath, factory, options) {
|
|
354
|
+
return createInteractiveListenGroupQueue({
|
|
355
|
+
...options,
|
|
356
|
+
resolver: factory(dbPath, LISTEN_HISTORY_LIMIT),
|
|
357
|
+
});
|
|
69
358
|
}
|
|
70
359
|
export function ListenStatus({ status, unseenCount }) {
|
|
71
360
|
return _jsxs(Text, { dimColor: true, children: [status, unseenCount > 0 ? ` · ↓ ${unseenCount} new messages` : ''] });
|
|
72
361
|
}
|
|
362
|
+
export function formatInteractiveListenSender(message) {
|
|
363
|
+
const sender = message.senderId == null || message.sender === String(message.senderId)
|
|
364
|
+
? message.sender
|
|
365
|
+
: `${message.sender} (${message.senderId})`;
|
|
366
|
+
return message.chatName == null ? sender : `${message.chatName} | ${sender}`;
|
|
367
|
+
}
|
|
368
|
+
export function formatInteractiveListenHeader(message) {
|
|
369
|
+
return `[${message.time}] #${message.msgId} ${formatInteractiveListenSender(message)}`;
|
|
370
|
+
}
|
|
371
|
+
export function ListenStatusArea({ status, unseenCount, autoDownload, }) {
|
|
372
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ListenStatus, { status: status, unseenCount: unseenCount }), autoDownload ? _jsx(Text, { dimColor: true, children: "Auto-download enabled" }) : null] }));
|
|
373
|
+
}
|
|
374
|
+
export function calculateListenMessagePaneHeight(terminalHeight, hasNote, autoDownload) {
|
|
375
|
+
const reservedLines = 7 + (hasNote ? 1 : 0) + (autoDownload ? 1 : 0);
|
|
376
|
+
return Math.max(2, terminalHeight - reservedLines);
|
|
377
|
+
}
|
|
73
378
|
export async function runInteractiveListen(write, run) {
|
|
74
|
-
|
|
379
|
+
write(DISABLE_MOUSE_REPORTING);
|
|
380
|
+
return run();
|
|
75
381
|
}
|
|
76
382
|
export async function renderInteractiveListen(options) {
|
|
77
383
|
await runInteractiveListen(process.stdout.write.bind(process.stdout), async () => {
|
|
@@ -79,7 +385,7 @@ export async function renderInteractiveListen(options) {
|
|
|
79
385
|
await app.waitUntilExit();
|
|
80
386
|
});
|
|
81
387
|
}
|
|
82
|
-
function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, showChatName, createClient, stopSignal, onRequestStop, }) {
|
|
388
|
+
export function InteractiveListen({ dbPath, chats, persist, retrySeconds, sendTo, showMedia, autoDownload, showChatName, createClient, stopSignal, onRequestStop, createReplyResolver, }) {
|
|
83
389
|
const { exit } = useApp();
|
|
84
390
|
const { stdout } = useStdout();
|
|
85
391
|
const terminalMetrics = useTerminalMetrics(stdout);
|
|
@@ -93,42 +399,97 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
93
399
|
const [downloadStates, setDownloadStates] = useState({});
|
|
94
400
|
const [scrollState, setScrollState] = useState({ offset: 0, unseenCount: 0 });
|
|
95
401
|
const [sendTargetLabel, setSendTargetLabel] = useState(sendTo == null ? '' : buildSendTargetLabel(sendTo));
|
|
402
|
+
const [knownGroup, setKnownGroup] = useState(undefined);
|
|
403
|
+
const clientRef = useRef(null);
|
|
404
|
+
const replyExecutionLockRef = useRef(false);
|
|
405
|
+
const inputGenerationRef = useRef(0);
|
|
406
|
+
const commandSelectionRef = useRef(0);
|
|
407
|
+
const knownGroupRef = useRef(undefined);
|
|
408
|
+
const groupLookupGenerationRef = useRef(0);
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
groupLookupGenerationRef.current++;
|
|
411
|
+
knownGroupRef.current = undefined;
|
|
412
|
+
setKnownGroup(undefined);
|
|
413
|
+
}, [sendTo]);
|
|
414
|
+
const groupCommand = useGroupCommand(useCallback(async (request, options) => {
|
|
415
|
+
const client = clientRef.current;
|
|
416
|
+
if (client == null)
|
|
417
|
+
return { ok: false, error: { code: 'connection_not_ready', message: 'Telegram connection is not ready.' } };
|
|
418
|
+
if (sendTo == null)
|
|
419
|
+
return { ok: false, error: { code: 'ambiguous_chat', message: 'Select exactly one target chat with --send-to.' } };
|
|
420
|
+
let knownGroup = knownGroupRef.current;
|
|
421
|
+
if (knownGroup == null) {
|
|
422
|
+
const lookup = ++groupLookupGenerationRef.current;
|
|
423
|
+
knownGroup = await client.groups.getGroup(sendTo);
|
|
424
|
+
if (lookup === groupLookupGenerationRef.current && clientRef.current === client) {
|
|
425
|
+
knownGroupRef.current = knownGroup;
|
|
426
|
+
setKnownGroup(knownGroup);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return executeGroupCommand(request, {
|
|
430
|
+
chat: sendTo,
|
|
431
|
+
groups: new GroupWriteService(client.groups),
|
|
432
|
+
confirmed: options?.confirmed ?? false,
|
|
433
|
+
confirmationTitle: options?.confirmationTitle,
|
|
434
|
+
knownGroup,
|
|
435
|
+
connectionReady: true,
|
|
436
|
+
targetAvailable: true,
|
|
437
|
+
targetCount: 1,
|
|
438
|
+
invalidateGroup: async () => {
|
|
439
|
+
knownGroupRef.current = undefined;
|
|
440
|
+
setKnownGroup(undefined);
|
|
441
|
+
if (request.key === 'chat delete' || request.key === 'chat leave')
|
|
442
|
+
return;
|
|
443
|
+
const lookup = ++groupLookupGenerationRef.current;
|
|
444
|
+
try {
|
|
445
|
+
const refreshed = await client.groups.getGroup(sendTo);
|
|
446
|
+
if (lookup === groupLookupGenerationRef.current && clientRef.current === client) {
|
|
447
|
+
knownGroupRef.current = refreshed;
|
|
448
|
+
setKnownGroup(refreshed);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (lookup === groupLookupGenerationRef.current && clientRef.current === client)
|
|
453
|
+
setNote(`Group updated; refresh failed: ${messageFromError(error)}`);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}, [sendTo]));
|
|
96
458
|
const terminalWidth = terminalMetrics.columns;
|
|
97
459
|
const terminalHeight = terminalMetrics.rows;
|
|
98
460
|
const previewColorDepth = interactiveListenPreviewColorDepth(terminalMetrics.colorDepth);
|
|
99
461
|
const contentWidth = listenContentWidth(terminalWidth);
|
|
100
462
|
const previewWidth = Math.max(1, Math.min(24, contentWidth - 2));
|
|
463
|
+
const renderContextRef = useRef({ showMedia, previewWidth, colorDepth: previewColorDepth, showChatName });
|
|
464
|
+
renderContextRef.current = { showMedia, previewWidth, colorDepth: previewColorDepth, showChatName };
|
|
101
465
|
const messageViewCacheRef = useRef(null);
|
|
102
466
|
if (messageViewCacheRef.current == null)
|
|
103
467
|
messageViewCacheRef.current = new ListenMessageViewCache();
|
|
104
|
-
const messages = useMemo(() => messageViewCacheRef.current.build(messageGroups,
|
|
105
|
-
|
|
106
|
-
previewWidth,
|
|
107
|
-
colorDepth: previewColorDepth,
|
|
108
|
-
showChatName,
|
|
109
|
-
}), [messageGroups, showMedia, previewWidth, previewColorDepth, showChatName]);
|
|
110
|
-
const reservedLines = 7 + (note ? 1 : 0);
|
|
111
|
-
const messagePaneHeight = Math.max(2, terminalHeight - reservedLines);
|
|
468
|
+
const messages = useMemo(() => messageViewCacheRef.current.build(messageGroups, renderContextRef.current), [messageGroups, showMedia, previewWidth, previewColorDepth, showChatName]);
|
|
469
|
+
const messagePaneHeight = calculateListenMessagePaneHeight(terminalHeight, note.length > 0, autoDownload);
|
|
112
470
|
const visibleMessages = takeListenViewport(messages, messagePaneHeight, scrollState.offset);
|
|
113
|
-
const
|
|
471
|
+
const closeGroupCommand = () => {
|
|
472
|
+
commandSelectionRef.current = 0;
|
|
473
|
+
groupCommand.close();
|
|
474
|
+
};
|
|
114
475
|
const albumAggregatorRef = useRef(null);
|
|
476
|
+
const autoDownloaderRef = useRef(null);
|
|
477
|
+
const pendingAttachmentKeysRef = useRef(new Set());
|
|
478
|
+
const operationControllerRef = useRef(null);
|
|
479
|
+
if (operationControllerRef.current == null)
|
|
480
|
+
operationControllerRef.current = createInteractiveOperationController();
|
|
115
481
|
const stoppingRef = useRef(false);
|
|
116
482
|
const seenRef = useRef(new Set());
|
|
117
483
|
const seenOrderRef = useRef([]);
|
|
118
|
-
const downloadableAttachments =
|
|
484
|
+
const downloadableAttachments = collectDownloadableAttachments(visibleMessages);
|
|
119
485
|
const selectedAttachment = downloadableAttachments[selectedAttachmentIndex] ?? downloadableAttachments[0];
|
|
120
|
-
const { visible: scrollbarVisible
|
|
486
|
+
const { visible: scrollbarVisible } = useTransientScrollbar();
|
|
121
487
|
const scrollbarGeometry = calculateScrollbar({
|
|
122
488
|
height: terminalHeight,
|
|
123
489
|
total: messages.length,
|
|
124
490
|
visible: visibleMessages.length,
|
|
125
491
|
offset: scrollState.offset,
|
|
126
492
|
});
|
|
127
|
-
const handleMouseScroll = useCallback((direction) => {
|
|
128
|
-
showScrollbar();
|
|
129
|
-
setScrollState((current) => applyScroll(current, direction, Math.max(0, messages.length - 1)));
|
|
130
|
-
}, [messages.length, showScrollbar]);
|
|
131
|
-
useMouseScroll(handleMouseScroll);
|
|
132
493
|
useEffect(() => {
|
|
133
494
|
const maxOffset = Math.max(0, messages.length - 1);
|
|
134
495
|
setScrollState((current) => {
|
|
@@ -138,11 +499,12 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
138
499
|
? current
|
|
139
500
|
: { offset, unseenCount };
|
|
140
501
|
});
|
|
141
|
-
const validAttachmentKeys = new Set(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
502
|
+
const validAttachmentKeys = new Set(collectDownloadableAttachments(messages).map((item) => item.key));
|
|
503
|
+
if (!showMedia)
|
|
504
|
+
pendingAttachmentKeysRef.current.clear();
|
|
505
|
+
setDownloadStates((current) => pruneAttachmentDownloadStates(current, validAttachmentKeys, pendingAttachmentKeysRef.current));
|
|
506
|
+
for (const key of validAttachmentKeys)
|
|
507
|
+
pendingAttachmentKeysRef.current.delete(key);
|
|
146
508
|
setSelectedAttachmentIndex((current) => Math.min(current, Math.max(0, downloadableAttachments.length - 1)));
|
|
147
509
|
if (downloadableAttachments.length === 0)
|
|
148
510
|
setFocus('input');
|
|
@@ -154,6 +516,139 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
154
516
|
stopListening();
|
|
155
517
|
return;
|
|
156
518
|
}
|
|
519
|
+
const modal = groupCommand.state;
|
|
520
|
+
if (modal.kind === 'confirm') {
|
|
521
|
+
if (key.escape) {
|
|
522
|
+
closeGroupCommand();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (key.upArrow || key.downArrow) {
|
|
526
|
+
groupCommand.setState({ ...modal, selectedIndex: modal.selectedIndex === 0 ? 1 : 0 });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (key.return) {
|
|
530
|
+
if (modal.selectedIndex === 0)
|
|
531
|
+
void groupCommand.runConfirmed(modal.request);
|
|
532
|
+
else
|
|
533
|
+
closeGroupCommand();
|
|
534
|
+
}
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (modal.kind === 'confirm-title') {
|
|
538
|
+
if (key.escape) {
|
|
539
|
+
if (modal.stage === 'title')
|
|
540
|
+
groupCommand.setState({ ...modal, stage: 'confirm', confirmText: '', mismatch: false });
|
|
541
|
+
else
|
|
542
|
+
closeGroupCommand();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (modal.stage === 'confirm') {
|
|
546
|
+
if (key.upArrow || key.downArrow)
|
|
547
|
+
groupCommand.setState({ ...modal, selectedIndex: modal.selectedIndex === 0 ? 1 : 0 });
|
|
548
|
+
else if (key.return)
|
|
549
|
+
modal.selectedIndex === 0 ? groupCommand.setState({ ...modal, stage: 'title' }) : closeGroupCommand();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (key.return) {
|
|
553
|
+
const client = clientRef.current;
|
|
554
|
+
if (client == null || sendTo == null) {
|
|
555
|
+
groupCommand.setState({ kind: 'error', message: 'Telegram connection is not ready.' });
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
const submittedTitle = modal.confirmText;
|
|
559
|
+
const lookup = ++groupLookupGenerationRef.current;
|
|
560
|
+
void client.groups.getGroup(sendTo).then((fresh) => {
|
|
561
|
+
if (lookup !== groupLookupGenerationRef.current || clientRef.current !== client)
|
|
562
|
+
return;
|
|
563
|
+
knownGroupRef.current = fresh;
|
|
564
|
+
setKnownGroup(fresh);
|
|
565
|
+
if (fresh.title === submittedTitle)
|
|
566
|
+
void groupCommand.runConfirmed(modal.request, submittedTitle);
|
|
567
|
+
else if ('confirmation' in modal.pending)
|
|
568
|
+
groupCommand.setState({
|
|
569
|
+
...modal,
|
|
570
|
+
pending: { ...modal.pending, confirmation: { ...modal.pending.confirmation, title: fresh.title, target: fresh.title } },
|
|
571
|
+
mismatch: true,
|
|
572
|
+
});
|
|
573
|
+
}).catch((error) => {
|
|
574
|
+
if (lookup === groupLookupGenerationRef.current && clientRef.current === client)
|
|
575
|
+
groupCommand.setState({ kind: 'error', message: messageFromError(error) });
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else if (key.backspace || key.delete)
|
|
580
|
+
groupCommand.setState({ ...modal, confirmText: modal.confirmText.slice(0, -1), mismatch: false });
|
|
581
|
+
else if (!key.ctrl && !key.meta && inputText)
|
|
582
|
+
groupCommand.setState({ ...modal, confirmText: modal.confirmText + inputText, mismatch: false });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (modal.kind === 'select-permissions') {
|
|
586
|
+
if (key.escape) {
|
|
587
|
+
closeGroupCommand();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (key.upArrow || key.downArrow) {
|
|
591
|
+
const delta = key.upArrow ? -1 : 1;
|
|
592
|
+
groupCommand.setState({ ...modal, selectedIndex: (modal.selectedIndex + delta + ADMIN_RIGHT_KEYS.length) % ADMIN_RIGHT_KEYS.length });
|
|
593
|
+
}
|
|
594
|
+
else if (inputText === ' ') {
|
|
595
|
+
const right = ADMIN_RIGHT_KEYS[modal.selectedIndex];
|
|
596
|
+
groupCommand.setState({ ...modal, selected: modal.selected.includes(right) ? modal.selected.filter(item => item !== right) : [...modal.selected, right], warning: undefined });
|
|
597
|
+
}
|
|
598
|
+
else if (key.return) {
|
|
599
|
+
if (modal.selected.length === 0)
|
|
600
|
+
groupCommand.setState({ ...modal, warning: 'Select at least one permission.' });
|
|
601
|
+
else {
|
|
602
|
+
const request = Object.freeze({ ...modal.request, values: Object.freeze({ ...modal.request.values, permissions: Object.freeze([...modal.selected]) }) });
|
|
603
|
+
void (async () => {
|
|
604
|
+
const result = await groupCommand.submit(`${modal.originalInput} ${modal.selected.join(',')}`, 0);
|
|
605
|
+
if (result.kind === 'pending' && 'confirmation' in result.pending)
|
|
606
|
+
groupCommand.setState({ kind: 'confirm', pending: result.pending, request, originalInput: modal.originalInput, selectedIndex: 1 });
|
|
607
|
+
})();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (groupCommand.state.kind === 'result') {
|
|
613
|
+
if (key.escape)
|
|
614
|
+
closeGroupCommand();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (key.escape && groupCommand.state.kind === 'error') {
|
|
618
|
+
closeGroupCommand();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const slashMode = input.trimStart().startsWith('/');
|
|
622
|
+
if (slashMode && key.escape) {
|
|
623
|
+
if (replyExecutionLockRef.current) {
|
|
624
|
+
inputGenerationRef.current++;
|
|
625
|
+
setSending(false);
|
|
626
|
+
}
|
|
627
|
+
closeGroupCommand();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (slashMode && (key.upArrow || key.downArrow)) {
|
|
631
|
+
const count = visibleListenCommandMatches(input).length;
|
|
632
|
+
const disabled = listenCommandMenuAvailability(input, knownGroup).map(Boolean);
|
|
633
|
+
const selectedIndex = moveListenCommandSelectionEnabled(commandSelectionRef.current, key.upArrow ? -1 : 1, disabled.slice(0, count));
|
|
634
|
+
commandSelectionRef.current = selectedIndex;
|
|
635
|
+
groupCommand.setState({ kind: 'menu', selectedIndex });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (slashMode && key.tab) {
|
|
639
|
+
const selected = commandSelectionRef.current;
|
|
640
|
+
const failure = listenCommandMenuAvailability(input, knownGroup)[selected];
|
|
641
|
+
if (failure && 'error' in failure) {
|
|
642
|
+
setNote(failure.error.message);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
inputGenerationRef.current++;
|
|
646
|
+
setInput(completeListenCommand(input, selected));
|
|
647
|
+
commandSelectionRef.current = 0;
|
|
648
|
+
groupCommand.setState({ kind: 'menu', selectedIndex: 0 });
|
|
649
|
+
setFocus('input');
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
157
652
|
if (key.tab) {
|
|
158
653
|
if (focus === 'attachments') {
|
|
159
654
|
setFocus('input');
|
|
@@ -181,17 +676,92 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
181
676
|
return;
|
|
182
677
|
}
|
|
183
678
|
if (key.return && selectedAttachment != null) {
|
|
184
|
-
|
|
679
|
+
const state = downloadStates[selectedAttachment.key] ?? { status: 'idle' };
|
|
680
|
+
if (canManuallyDownload(state))
|
|
681
|
+
void downloadAttachment(selectedAttachment);
|
|
185
682
|
}
|
|
186
683
|
return;
|
|
187
684
|
}
|
|
188
|
-
if (sending)
|
|
685
|
+
if (sending || groupCommand.state.kind === 'executing')
|
|
189
686
|
return;
|
|
190
687
|
if (key.return) {
|
|
688
|
+
if (slashMode) {
|
|
689
|
+
const selected = commandSelectionRef.current;
|
|
690
|
+
const failure = listenCommandMenuAvailability(input, knownGroup)[selected];
|
|
691
|
+
if (failure && 'error' in failure) {
|
|
692
|
+
setNote(failure.error.message);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const match = visibleListenCommandMatches(input)[selected];
|
|
696
|
+
if (!match) {
|
|
697
|
+
setNote('No matching command.');
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const parsed = parseSelectedListenCommand(input, match);
|
|
701
|
+
if (parsed.kind === 'complete') {
|
|
702
|
+
inputGenerationRef.current++;
|
|
703
|
+
setInput(parsed.input);
|
|
704
|
+
commandSelectionRef.current = 0;
|
|
705
|
+
groupCommand.setState({ kind: 'menu', selectedIndex: 0 });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (parsed.kind === 'error') {
|
|
709
|
+
setNote(parsed.usage == null ? parsed.message : `${parsed.message} · usage: /${parsed.usage}`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (parsed.kind === 'reply') {
|
|
713
|
+
if (replyExecutionLockRef.current)
|
|
714
|
+
return;
|
|
715
|
+
if (sendTo == null) {
|
|
716
|
+
setNote('set --send-to before replying');
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const client = clientRef.current;
|
|
720
|
+
if (client == null) {
|
|
721
|
+
setNote('connection is not ready');
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
replyExecutionLockRef.current = true;
|
|
725
|
+
const ownedGeneration = inputGenerationRef.current;
|
|
726
|
+
const originalInput = input;
|
|
727
|
+
setSending(true);
|
|
728
|
+
setNote('sending...');
|
|
729
|
+
void executeListenReply(client, sendTo, parsed.command).then((sentMessages) => {
|
|
730
|
+
if (ownedGeneration !== inputGenerationRef.current)
|
|
731
|
+
return;
|
|
732
|
+
for (const sentMessage of sentMessages)
|
|
733
|
+
acceptListenMessage(sentMessage, seenRef.current, seenOrderRef.current, (message) => {
|
|
734
|
+
registerPendingAttachmentKeys(pendingAttachmentKeysRef.current, message, showMedia);
|
|
735
|
+
autoDownloaderRef.current?.enqueue(message);
|
|
736
|
+
albumAggregatorRef.current?.add(message);
|
|
737
|
+
});
|
|
738
|
+
setInput('');
|
|
739
|
+
setNote(`replied to #${parsed.command.reply}`);
|
|
740
|
+
}).catch((error) => {
|
|
741
|
+
if (ownedGeneration === inputGenerationRef.current) {
|
|
742
|
+
setInput(originalInput);
|
|
743
|
+
setNote(`send failed: ${messageFromError(error)}`);
|
|
744
|
+
}
|
|
745
|
+
}).finally(() => {
|
|
746
|
+
replyExecutionLockRef.current = false;
|
|
747
|
+
if (ownedGeneration === inputGenerationRef.current)
|
|
748
|
+
setSending(false);
|
|
749
|
+
});
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
void groupCommand.submitParsed(parsed.request, input, selected).then((outcome) => {
|
|
753
|
+
if (!outcome.applied)
|
|
754
|
+
return;
|
|
755
|
+
if (outcome.kind === 'result' && outcome.result.ok)
|
|
756
|
+
setInput('');
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
191
760
|
void sendMessage(input);
|
|
192
761
|
return;
|
|
193
762
|
}
|
|
194
763
|
if (key.backspace || key.delete) {
|
|
764
|
+
inputGenerationRef.current++;
|
|
195
765
|
setInput((current) => current.slice(0, -1));
|
|
196
766
|
return;
|
|
197
767
|
}
|
|
@@ -199,10 +769,21 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
199
769
|
return;
|
|
200
770
|
}
|
|
201
771
|
if (!key.ctrl && !key.meta && inputText.length > 0) {
|
|
202
|
-
|
|
772
|
+
inputGenerationRef.current++;
|
|
773
|
+
setInput((current) => {
|
|
774
|
+
const next = current + inputText;
|
|
775
|
+
if (next.trimStart().startsWith('/')) {
|
|
776
|
+
setFocus('input');
|
|
777
|
+
commandSelectionRef.current = 0;
|
|
778
|
+
groupCommand.setState({ kind: 'menu', selectedIndex: 0 });
|
|
779
|
+
}
|
|
780
|
+
return next;
|
|
781
|
+
});
|
|
203
782
|
}
|
|
204
783
|
});
|
|
205
784
|
useEffect(() => {
|
|
785
|
+
const generation = operationControllerRef.current.beginGeneration();
|
|
786
|
+
const isActive = generation.isActive;
|
|
206
787
|
if (stopSignal.aborted) {
|
|
207
788
|
exit();
|
|
208
789
|
return;
|
|
@@ -211,105 +792,149 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
211
792
|
stopListening();
|
|
212
793
|
};
|
|
213
794
|
stopSignal.addEventListener('abort', stopFromSignal);
|
|
795
|
+
const groupQueue = createInteractiveListenRuntime(dbPath, createReplyResolver ?? createListenReplyResolver, {
|
|
796
|
+
isActive,
|
|
797
|
+
onGroup: (group) => {
|
|
798
|
+
setScrollState((current) => applyMessageArrival(current));
|
|
799
|
+
setMessageGroups((current) => [...current, group].slice(-LISTEN_HISTORY_LIMIT));
|
|
800
|
+
},
|
|
801
|
+
onError: (error) => {
|
|
802
|
+
if (!isActive())
|
|
803
|
+
return;
|
|
804
|
+
setStatus(`listen failed: ${messageFromError(error)}`);
|
|
805
|
+
onRequestStop();
|
|
806
|
+
exit();
|
|
807
|
+
},
|
|
808
|
+
});
|
|
214
809
|
const albumAggregator = new ListenAlbumAggregator({
|
|
215
810
|
emit: (group) => {
|
|
216
|
-
|
|
217
|
-
|
|
811
|
+
if (!isActive())
|
|
812
|
+
return;
|
|
813
|
+
groupQueue.enqueue(group);
|
|
218
814
|
},
|
|
219
815
|
});
|
|
220
816
|
albumAggregatorRef.current = albumAggregator;
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
if (label != null)
|
|
231
|
-
setSendTargetLabel(label);
|
|
232
|
-
});
|
|
817
|
+
const autoDownloader = autoDownload
|
|
818
|
+
? new AutoDownloadCoordinator({
|
|
819
|
+
onEvent: (event) => {
|
|
820
|
+
if (!isActive())
|
|
821
|
+
return;
|
|
822
|
+
const ownership = operationControllerRef.current.claimDownload(event.key);
|
|
823
|
+
setDownloadStates((current) => applyAutoDownloadEvent(current, event, showMedia));
|
|
824
|
+
if (event.status === 'completed' || event.status === 'failed' || event.status === 'cancelled') {
|
|
825
|
+
ownership.release();
|
|
233
826
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (!persist) {
|
|
264
|
-
setStatus('stopped');
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
setStatus(`disconnected, retry in ${retrySeconds}s...`);
|
|
268
|
-
retry = true;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
if (!persist) {
|
|
273
|
-
setStatus(`listen failed: ${messageFromError(error)}`);
|
|
274
|
-
break;
|
|
827
|
+
},
|
|
828
|
+
})
|
|
829
|
+
: null;
|
|
830
|
+
autoDownloaderRef.current = autoDownloader;
|
|
831
|
+
void runInteractiveAutoDownloadLifecycle({
|
|
832
|
+
autoDownload,
|
|
833
|
+
chats,
|
|
834
|
+
persist,
|
|
835
|
+
retrySeconds,
|
|
836
|
+
signal: stopSignal,
|
|
837
|
+
createClient,
|
|
838
|
+
createCoordinator: () => autoDownloader,
|
|
839
|
+
onCoordinator: (coordinator) => {
|
|
840
|
+
if (isActive())
|
|
841
|
+
autoDownloaderRef.current = coordinator;
|
|
842
|
+
},
|
|
843
|
+
onClient: (client) => {
|
|
844
|
+
if (!isActive())
|
|
845
|
+
return;
|
|
846
|
+
groupLookupGenerationRef.current++;
|
|
847
|
+
knownGroupRef.current = undefined;
|
|
848
|
+
setKnownGroup(undefined);
|
|
849
|
+
clientRef.current = client;
|
|
850
|
+
if (client != null && sendTo != null) {
|
|
851
|
+
const lookup = ++groupLookupGenerationRef.current;
|
|
852
|
+
void client.groups.getGroup(sendTo).then((group) => {
|
|
853
|
+
if (isActive() && lookup === groupLookupGenerationRef.current && clientRef.current === client) {
|
|
854
|
+
knownGroupRef.current = group;
|
|
855
|
+
setKnownGroup(group);
|
|
275
856
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
await client.close().catch(() => undefined);
|
|
282
|
-
if (clientRef.current === client)
|
|
283
|
-
clientRef.current = null;
|
|
284
|
-
}
|
|
285
|
-
if (retry) {
|
|
286
|
-
await sleep(retrySeconds);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
857
|
+
}).catch(() => undefined);
|
|
858
|
+
void resolveSendTargetLabel(client, sendTo).then((label) => {
|
|
859
|
+
if (isActive() && label != null)
|
|
860
|
+
setSendTargetLabel(label);
|
|
861
|
+
});
|
|
290
862
|
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (!
|
|
294
|
-
|
|
295
|
-
|
|
863
|
+
},
|
|
864
|
+
acceptMessage: (message) => {
|
|
865
|
+
if (!isActive())
|
|
866
|
+
return false;
|
|
867
|
+
return acceptListenMessage(message, seenRef.current, seenOrderRef.current, () => undefined);
|
|
868
|
+
},
|
|
869
|
+
onBeforeEnqueue: (message) => {
|
|
870
|
+
if (!isActive())
|
|
871
|
+
return;
|
|
872
|
+
registerPendingAttachmentKeys(pendingAttachmentKeysRef.current, message, showMedia);
|
|
873
|
+
},
|
|
874
|
+
onMessage: (message) => {
|
|
875
|
+
if (!isActive())
|
|
876
|
+
return;
|
|
877
|
+
if (sendTo != null) {
|
|
878
|
+
const inferred = inferSendTargetLabel(sendTo, message);
|
|
879
|
+
if (inferred != null)
|
|
880
|
+
setSendTargetLabel(inferred);
|
|
296
881
|
}
|
|
882
|
+
albumAggregator.add(message);
|
|
883
|
+
},
|
|
884
|
+
onStatus: (next) => {
|
|
885
|
+
if (!isActive())
|
|
886
|
+
return;
|
|
887
|
+
setStatus(next === 'connecting'
|
|
888
|
+
? 'connecting...'
|
|
889
|
+
: next === 'disconnected'
|
|
890
|
+
? `disconnected, retry in ${retrySeconds}s...`
|
|
891
|
+
: next);
|
|
892
|
+
},
|
|
893
|
+
onError: (error) => {
|
|
894
|
+
if (!isActive())
|
|
895
|
+
return;
|
|
896
|
+
if (persist)
|
|
897
|
+
setNote(`listen failed: ${messageFromError(error)}`);
|
|
898
|
+
else
|
|
899
|
+
setStatus(`listen failed: ${messageFromError(error)}`);
|
|
900
|
+
},
|
|
901
|
+
flush: () => {
|
|
902
|
+
if (isActive())
|
|
903
|
+
albumAggregator.flush();
|
|
904
|
+
},
|
|
905
|
+
}).finally(() => {
|
|
906
|
+
if (isActive() && !stopSignal.aborted) {
|
|
907
|
+
onRequestStop();
|
|
908
|
+
exit();
|
|
297
909
|
}
|
|
298
|
-
};
|
|
299
|
-
void run();
|
|
910
|
+
});
|
|
300
911
|
return () => {
|
|
912
|
+
groupLookupGenerationRef.current += 1;
|
|
913
|
+
knownGroupRef.current = undefined;
|
|
301
914
|
stopSignal.removeEventListener('abort', stopFromSignal);
|
|
915
|
+
albumAggregator.flush();
|
|
916
|
+
generation.dispose();
|
|
302
917
|
albumAggregator.dispose();
|
|
918
|
+
void groupQueue.close();
|
|
303
919
|
if (albumAggregatorRef.current === albumAggregator)
|
|
304
920
|
albumAggregatorRef.current = null;
|
|
305
921
|
void clientRef.current?.close().catch(() => undefined);
|
|
306
922
|
clientRef.current = null;
|
|
923
|
+
autoDownloader?.stop();
|
|
924
|
+
void autoDownloader?.waitForActive();
|
|
925
|
+
if (autoDownloaderRef.current === autoDownloader)
|
|
926
|
+
autoDownloaderRef.current = null;
|
|
307
927
|
};
|
|
308
|
-
}, [chats, createClient, persist, retrySeconds, sendTo, showMedia, exit, stopSignal, onRequestStop]);
|
|
928
|
+
}, [autoDownload, chats, createClient, createReplyResolver, dbPath, persist, retrySeconds, sendTo, showMedia, exit, stopSignal, onRequestStop]);
|
|
309
929
|
const sendMessage = async (text) => {
|
|
310
930
|
const trimmed = text.trim();
|
|
311
931
|
if (!trimmed)
|
|
312
932
|
return;
|
|
933
|
+
const command = parseListenComposerInput(trimmed);
|
|
934
|
+
if (command.kind === 'error') {
|
|
935
|
+
setNote(command.error);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
313
938
|
if (sendTo == null) {
|
|
314
939
|
setNote('set --send-to before sending');
|
|
315
940
|
return;
|
|
@@ -319,51 +944,68 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
319
944
|
setNote('connection is not ready');
|
|
320
945
|
return;
|
|
321
946
|
}
|
|
947
|
+
const isCurrent = operationControllerRef.current.beginSend();
|
|
322
948
|
setInput('');
|
|
323
949
|
setSending(true);
|
|
324
950
|
setNote('sending...');
|
|
325
951
|
try {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
952
|
+
const sentMessages = command.kind === 'reply'
|
|
953
|
+
? await executeListenReply(client, sendTo, command)
|
|
954
|
+
: [await client.sendMessage({
|
|
955
|
+
chat: sendTo,
|
|
956
|
+
message: command.content,
|
|
957
|
+
linkPreview: true,
|
|
958
|
+
})].flatMap(({ sent_message: message }) => message == null ? [] : [message]);
|
|
959
|
+
if (isCurrent()) {
|
|
960
|
+
for (const sentMessage of sentMessages)
|
|
961
|
+
acceptListenMessage(sentMessage, seenRef.current, seenOrderRef.current, (message) => {
|
|
962
|
+
registerPendingAttachmentKeys(pendingAttachmentKeysRef.current, message, showMedia);
|
|
963
|
+
autoDownloaderRef.current?.enqueue(message);
|
|
964
|
+
albumAggregatorRef.current?.add(message);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
if (isCurrent())
|
|
968
|
+
setNote(command.kind === 'reply' ? `replied to #${command.reply}` : 'sent');
|
|
332
969
|
}
|
|
333
970
|
catch (error) {
|
|
334
|
-
|
|
971
|
+
if (isCurrent())
|
|
972
|
+
setNote(`send failed: ${messageFromError(error)}`);
|
|
335
973
|
}
|
|
336
974
|
finally {
|
|
337
|
-
|
|
975
|
+
if (isCurrent())
|
|
976
|
+
setSending(false);
|
|
338
977
|
}
|
|
339
978
|
};
|
|
340
979
|
const downloadAttachment = async (item) => {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
980
|
+
const ownership = operationControllerRef.current.beginDownload(item.key);
|
|
981
|
+
const isCurrent = ownership.isCurrent;
|
|
982
|
+
await runOwnedAttachmentOperation(ownership, async () => {
|
|
983
|
+
const client = clientRef.current;
|
|
984
|
+
if (!client)
|
|
985
|
+
throw new Error('not connected');
|
|
986
|
+
const destination = resolveAttachmentDestination({
|
|
987
|
+
homeDir: homedir(),
|
|
988
|
+
fileName: attachmentFileName(item.attachment),
|
|
989
|
+
exists: existsSync,
|
|
990
|
+
});
|
|
991
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
992
|
+
if (isCurrent())
|
|
993
|
+
setDownloadStates((current) => ({ ...current, [item.key]: { status: 'downloading', progress: 0 } }));
|
|
354
994
|
await client.downloadMessageMedia({
|
|
355
995
|
...attachmentDownloadTarget(item.attachment),
|
|
356
996
|
destination,
|
|
357
997
|
onProgress: (downloaded, total) => {
|
|
998
|
+
if (!isCurrent())
|
|
999
|
+
return;
|
|
358
1000
|
const progress = Number.isFinite(total) && total > 0 ? Math.round(downloaded / total * 100) : null;
|
|
359
1001
|
setDownloadStates((current) => ({ ...current, [item.key]: { status: 'downloading', progress } }));
|
|
360
1002
|
},
|
|
361
1003
|
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
1004
|
+
if (isCurrent())
|
|
1005
|
+
setDownloadStates((current) => ({ ...current, [item.key]: { status: 'completed', path: destination } }));
|
|
1006
|
+
}, (error) => {
|
|
365
1007
|
setDownloadStates((current) => ({ ...current, [item.key]: { status: 'failed', error: messageFromError(error) } }));
|
|
366
|
-
}
|
|
1008
|
+
});
|
|
367
1009
|
};
|
|
368
1010
|
const stopListening = () => {
|
|
369
1011
|
if (stoppingRef.current)
|
|
@@ -376,12 +1018,16 @@ function InteractiveListen({ chats, persist, retrySeconds, sendTo, showMedia, sh
|
|
|
376
1018
|
else
|
|
377
1019
|
setTimeout(exit, 0);
|
|
378
1020
|
onRequestStop();
|
|
1021
|
+
autoDownloaderRef.current?.stop();
|
|
379
1022
|
clientRef.current?.close().catch(() => undefined);
|
|
380
1023
|
};
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
1024
|
+
const permissionState = groupCommand.state.kind === 'select-permissions' ? groupCommand.state : null;
|
|
1025
|
+
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
|
+
? _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) => {
|
|
1028
|
+
const attachmentKey = attachmentDownloadKeyAt(message.media, mediaIndex);
|
|
1029
|
+
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' : 'Enter send · /reply <id> ... · Tab attachments · Ctrl+C exit' })] })] }) : null] }), _jsx(ListenScrollbar, { height: terminalHeight, visible: scrollbarVisible, geometry: scrollbarGeometry })] }));
|
|
385
1031
|
}
|
|
386
1032
|
export class ListenMessageViewCache {
|
|
387
1033
|
entries = new Map();
|
|
@@ -391,13 +1037,16 @@ export class ListenMessageViewCache {
|
|
|
391
1037
|
build(groups, context) {
|
|
392
1038
|
const previewWidth = normalizedPreviewWidth(context.previewWidth);
|
|
393
1039
|
const nextEntries = new Map();
|
|
394
|
-
const messages = groups.map((
|
|
1040
|
+
const messages = groups.map((input) => {
|
|
1041
|
+
const group = Array.isArray(input) ? input : input.messages;
|
|
1042
|
+
const replyContext = Array.isArray(input) ? context.replyContext : input.replyContext;
|
|
395
1043
|
const first = group[0];
|
|
396
1044
|
if (first == null)
|
|
397
1045
|
throw new Error('Cannot render an empty listen message group');
|
|
398
|
-
const key = `${first.chat_id}:${first.msg_id}
|
|
1046
|
+
const key = Array.isArray(input) ? `${first.chat_id}:${first.msg_id}` : input.key;
|
|
399
1047
|
const cached = this.entries.get(key);
|
|
400
1048
|
if (cached?.group === group
|
|
1049
|
+
&& cached.replyContext === replyContext
|
|
401
1050
|
&& cached.showMedia === context.showMedia
|
|
402
1051
|
&& cached.previewWidth === previewWidth
|
|
403
1052
|
&& cached.colorDepth === context.colorDepth
|
|
@@ -405,9 +1054,10 @@ export class ListenMessageViewCache {
|
|
|
405
1054
|
nextEntries.set(key, cached);
|
|
406
1055
|
return cached.message;
|
|
407
1056
|
}
|
|
408
|
-
const message = toListenMessage(group, { ...context, previewWidth });
|
|
1057
|
+
const message = toListenMessage(group, { ...context, previewWidth, replyContext });
|
|
409
1058
|
nextEntries.set(key, {
|
|
410
1059
|
group,
|
|
1060
|
+
replyContext,
|
|
411
1061
|
showMedia: context.showMedia,
|
|
412
1062
|
previewWidth,
|
|
413
1063
|
colorDepth: context.colorDepth,
|
|
@@ -427,7 +1077,7 @@ export function pruneListenMessageGroups(groups, limit = LISTEN_HISTORY_LIMIT) {
|
|
|
427
1077
|
return {
|
|
428
1078
|
groups: groups.slice(removeCount),
|
|
429
1079
|
removedKeys: removed.flatMap((group) => {
|
|
430
|
-
const first = group[0];
|
|
1080
|
+
const first = Array.isArray(group) ? group[0] : group.messages[0];
|
|
431
1081
|
return first == null ? [] : [`${first.chat_id}:${first.msg_id}`];
|
|
432
1082
|
}),
|
|
433
1083
|
};
|
|
@@ -439,8 +1089,8 @@ export function toListenMessage(messages, context) {
|
|
|
439
1089
|
const renderContext = typeof context === 'boolean'
|
|
440
1090
|
? { showMedia: context, previewWidth: 1, colorDepth: 1, showChatName: false }
|
|
441
1091
|
: context;
|
|
442
|
-
const { showMedia, previewWidth, colorDepth, showChatName = false } = renderContext;
|
|
443
|
-
const formatted = buildListenMessage(messages, { showMedia, showChatName });
|
|
1092
|
+
const { showMedia, previewWidth, colorDepth, showChatName = false, replyContext } = renderContext;
|
|
1093
|
+
const formatted = buildListenMessage(messages, { showMedia, showChatName, replyContext });
|
|
444
1094
|
const decodePreview = renderContext.decodePreview ?? decodeImagePreview;
|
|
445
1095
|
const media = formatted.media.map((attachment) => {
|
|
446
1096
|
if (attachment.previewJpegBase64 == null || colorDepth < 24)
|
|
@@ -461,35 +1111,20 @@ export function toListenMessage(messages, context) {
|
|
|
461
1111
|
function normalizedPreviewWidth(previewWidth) {
|
|
462
1112
|
return Math.max(1, Math.min(24, previewWidth));
|
|
463
1113
|
}
|
|
464
|
-
function
|
|
465
|
-
return messages.flatMap((message) => message.media.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (item.attachment.fileName != null)
|
|
473
|
-
return item.attachment.fileName;
|
|
474
|
-
const extension = MEDIA_EXTENSIONS[item.attachment.kind] ?? 'bin';
|
|
475
|
-
return `${item.attachment.chatId}-${item.attachment.messageId}.${extension}`;
|
|
476
|
-
}
|
|
477
|
-
export function attachmentDownloadTarget(attachment) {
|
|
478
|
-
return { chat: attachment.chatId, msgId: attachment.messageId };
|
|
1114
|
+
export function collectDownloadableAttachments(messages) {
|
|
1115
|
+
return messages.flatMap((message) => message.media.flatMap((attachment, index) => (attachment.downloadable === true
|
|
1116
|
+
? [{
|
|
1117
|
+
key: attachmentDownloadKeyAt(message.media, index),
|
|
1118
|
+
message,
|
|
1119
|
+
attachment,
|
|
1120
|
+
}]
|
|
1121
|
+
: [])));
|
|
479
1122
|
}
|
|
1123
|
+
export { attachmentDownloadTarget } from '../../services/listen-attachment.js';
|
|
480
1124
|
export function flushListenBeforeExit(aggregator, exit) {
|
|
481
1125
|
aggregator.flush();
|
|
482
1126
|
setTimeout(exit, 0);
|
|
483
1127
|
}
|
|
484
|
-
const MEDIA_EXTENSIONS = {
|
|
485
|
-
Photo: 'jpg',
|
|
486
|
-
Video: 'mp4',
|
|
487
|
-
Audio: 'mp3',
|
|
488
|
-
Voice: 'ogg',
|
|
489
|
-
Sticker: 'webp',
|
|
490
|
-
Animation: 'mp4',
|
|
491
|
-
Document: 'bin',
|
|
492
|
-
};
|
|
493
1128
|
function messageFromError(error) {
|
|
494
1129
|
return error instanceof Error ? error.message : String(error);
|
|
495
1130
|
}
|