openzca 0.1.48 → 0.1.50
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 +90 -2
- package/dist/cli.js +3142 -153
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { spawn as spawn2 } from "child_process";
|
|
6
6
|
import fsSync from "fs";
|
|
7
|
-
import
|
|
7
|
+
import fs6 from "fs/promises";
|
|
8
8
|
import net from "net";
|
|
9
9
|
import os4 from "os";
|
|
10
|
-
import
|
|
10
|
+
import path6 from "path";
|
|
11
|
+
import readline from "readline/promises";
|
|
11
12
|
import util from "util";
|
|
12
13
|
import { Command } from "commander";
|
|
13
14
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
Gender,
|
|
16
17
|
Reactions,
|
|
17
18
|
ReviewPendingMemberRequestStatus,
|
|
18
|
-
ThreadType as
|
|
19
|
+
ThreadType as ThreadType3
|
|
19
20
|
} from "zca-js";
|
|
20
21
|
|
|
21
22
|
// src/lib/store.ts
|
|
@@ -210,10 +211,1232 @@ async function clearCache(profileName) {
|
|
|
210
211
|
await fs.rm(getCacheMetaPath(profileName), { force: true });
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
// src/lib/
|
|
214
|
+
// src/lib/db.ts
|
|
215
|
+
import crypto from "crypto";
|
|
214
216
|
import fs2 from "fs/promises";
|
|
215
|
-
import { spawn } from "child_process";
|
|
216
217
|
import path2 from "path";
|
|
218
|
+
import { open } from "sqlite";
|
|
219
|
+
import sqlite3 from "sqlite3";
|
|
220
|
+
var DB_CONFIG_FILE = "db.json";
|
|
221
|
+
var DB_FILENAME = "messages.sqlite";
|
|
222
|
+
var connections = /* @__PURE__ */ new Map();
|
|
223
|
+
var writeQueues = /* @__PURE__ */ new Map();
|
|
224
|
+
function nowIso2() {
|
|
225
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
226
|
+
}
|
|
227
|
+
function normalizeId(value) {
|
|
228
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
229
|
+
return String(Math.trunc(value));
|
|
230
|
+
}
|
|
231
|
+
if (typeof value !== "string") {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
return value.trim();
|
|
235
|
+
}
|
|
236
|
+
function normalizeOptionalText(value) {
|
|
237
|
+
if (typeof value !== "string") {
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
const trimmed = value.trim();
|
|
241
|
+
return trimmed || void 0;
|
|
242
|
+
}
|
|
243
|
+
function normalizeSearchText(value) {
|
|
244
|
+
return value.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().trim();
|
|
245
|
+
}
|
|
246
|
+
function globToRegex(pattern) {
|
|
247
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
248
|
+
const regexSource = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
249
|
+
return new RegExp(`^${regexSource}$`);
|
|
250
|
+
}
|
|
251
|
+
function matchesSearchPattern(value, query) {
|
|
252
|
+
const normalizedValue = normalizeSearchText(value);
|
|
253
|
+
if (query.includes("*") || query.includes("?")) {
|
|
254
|
+
return globToRegex(query).test(normalizedValue);
|
|
255
|
+
}
|
|
256
|
+
return normalizedValue.includes(query);
|
|
257
|
+
}
|
|
258
|
+
function safeJsonStringify(value) {
|
|
259
|
+
if (value === void 0) {
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
return JSON.stringify(value);
|
|
263
|
+
}
|
|
264
|
+
function defaultDbPath(profile) {
|
|
265
|
+
return path2.join(getProfileDir(profile), DB_FILENAME);
|
|
266
|
+
}
|
|
267
|
+
function getDbConfigPath(profile) {
|
|
268
|
+
return path2.join(getProfileDir(profile), DB_CONFIG_FILE);
|
|
269
|
+
}
|
|
270
|
+
async function readDbConfig(profile) {
|
|
271
|
+
const configPath = getDbConfigPath(profile);
|
|
272
|
+
try {
|
|
273
|
+
const raw = await fs2.readFile(configPath, "utf8");
|
|
274
|
+
const parsed = JSON.parse(raw);
|
|
275
|
+
return {
|
|
276
|
+
enabled: Boolean(parsed.enabled),
|
|
277
|
+
path: normalizeOptionalText(parsed.path),
|
|
278
|
+
updatedAt: normalizeOptionalText(parsed.updatedAt) ?? nowIso2()
|
|
279
|
+
};
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const code = error.code;
|
|
282
|
+
if (code === "ENOENT") {
|
|
283
|
+
return {
|
|
284
|
+
enabled: false,
|
|
285
|
+
updatedAt: nowIso2()
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function writeDbConfig(profile, config) {
|
|
292
|
+
const configPath = getDbConfigPath(profile);
|
|
293
|
+
await fs2.mkdir(path2.dirname(configPath), { recursive: true });
|
|
294
|
+
await fs2.writeFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
295
|
+
`, "utf8");
|
|
296
|
+
}
|
|
297
|
+
async function enableDb(profile, customPath) {
|
|
298
|
+
const config = {
|
|
299
|
+
enabled: true,
|
|
300
|
+
path: normalizeOptionalText(customPath),
|
|
301
|
+
updatedAt: nowIso2()
|
|
302
|
+
};
|
|
303
|
+
await writeDbConfig(profile, config);
|
|
304
|
+
await getDb(profile);
|
|
305
|
+
return config;
|
|
306
|
+
}
|
|
307
|
+
async function disableDb(profile) {
|
|
308
|
+
const existing = await readDbConfig(profile);
|
|
309
|
+
const config = {
|
|
310
|
+
enabled: false,
|
|
311
|
+
path: existing.path,
|
|
312
|
+
updatedAt: nowIso2()
|
|
313
|
+
};
|
|
314
|
+
await writeDbConfig(profile, config);
|
|
315
|
+
return config;
|
|
316
|
+
}
|
|
317
|
+
async function isDbEnabled(profile) {
|
|
318
|
+
const config = await readDbConfig(profile);
|
|
319
|
+
return config.enabled;
|
|
320
|
+
}
|
|
321
|
+
async function resolveDbPath(profile) {
|
|
322
|
+
const config = await readDbConfig(profile);
|
|
323
|
+
const configured = normalizeOptionalText(config.path);
|
|
324
|
+
if (!configured) {
|
|
325
|
+
return defaultDbPath(profile);
|
|
326
|
+
}
|
|
327
|
+
return path2.isAbsolute(configured) ? configured : path2.resolve(getProfileDir(profile), configured);
|
|
328
|
+
}
|
|
329
|
+
async function migrateDb(db) {
|
|
330
|
+
await db.exec(`
|
|
331
|
+
PRAGMA journal_mode = WAL;
|
|
332
|
+
PRAGMA foreign_keys = ON;
|
|
333
|
+
|
|
334
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
335
|
+
profile TEXT NOT NULL,
|
|
336
|
+
scope_thread_id TEXT NOT NULL,
|
|
337
|
+
raw_thread_id TEXT NOT NULL,
|
|
338
|
+
thread_type TEXT NOT NULL,
|
|
339
|
+
peer_id TEXT,
|
|
340
|
+
title TEXT,
|
|
341
|
+
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
342
|
+
is_hidden INTEGER NOT NULL DEFAULT 0,
|
|
343
|
+
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
344
|
+
raw_json TEXT,
|
|
345
|
+
created_at TEXT NOT NULL,
|
|
346
|
+
updated_at TEXT NOT NULL,
|
|
347
|
+
PRIMARY KEY (profile, scope_thread_id)
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
CREATE TABLE IF NOT EXISTS thread_members (
|
|
351
|
+
profile TEXT NOT NULL,
|
|
352
|
+
scope_thread_id TEXT NOT NULL,
|
|
353
|
+
user_id TEXT NOT NULL,
|
|
354
|
+
display_name TEXT,
|
|
355
|
+
zalo_name TEXT,
|
|
356
|
+
avatar TEXT,
|
|
357
|
+
account_status INTEGER,
|
|
358
|
+
member_type INTEGER,
|
|
359
|
+
raw_json TEXT,
|
|
360
|
+
snapshot_at_ms INTEGER NOT NULL,
|
|
361
|
+
created_at TEXT NOT NULL,
|
|
362
|
+
updated_at TEXT NOT NULL,
|
|
363
|
+
PRIMARY KEY (profile, scope_thread_id, user_id)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
CREATE TABLE IF NOT EXISTS friends (
|
|
367
|
+
profile TEXT NOT NULL,
|
|
368
|
+
user_id TEXT NOT NULL,
|
|
369
|
+
display_name TEXT,
|
|
370
|
+
zalo_name TEXT,
|
|
371
|
+
avatar TEXT,
|
|
372
|
+
account_status INTEGER,
|
|
373
|
+
raw_json TEXT,
|
|
374
|
+
created_at TEXT NOT NULL,
|
|
375
|
+
updated_at TEXT NOT NULL,
|
|
376
|
+
PRIMARY KEY (profile, user_id)
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
CREATE TABLE IF NOT EXISTS self_profiles (
|
|
380
|
+
profile TEXT NOT NULL,
|
|
381
|
+
user_id TEXT NOT NULL,
|
|
382
|
+
display_name TEXT,
|
|
383
|
+
info_json TEXT,
|
|
384
|
+
created_at TEXT NOT NULL,
|
|
385
|
+
updated_at TEXT NOT NULL,
|
|
386
|
+
PRIMARY KEY (profile)
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
390
|
+
profile TEXT NOT NULL,
|
|
391
|
+
message_uid TEXT NOT NULL,
|
|
392
|
+
scope_thread_id TEXT NOT NULL,
|
|
393
|
+
raw_thread_id TEXT NOT NULL,
|
|
394
|
+
thread_type TEXT NOT NULL,
|
|
395
|
+
msg_id TEXT,
|
|
396
|
+
cli_msg_id TEXT,
|
|
397
|
+
action_id TEXT,
|
|
398
|
+
sender_id TEXT,
|
|
399
|
+
sender_name TEXT,
|
|
400
|
+
to_id TEXT,
|
|
401
|
+
timestamp_ms INTEGER NOT NULL,
|
|
402
|
+
msg_type TEXT,
|
|
403
|
+
content_text TEXT,
|
|
404
|
+
content_json TEXT,
|
|
405
|
+
quote_msg_id TEXT,
|
|
406
|
+
quote_cli_msg_id TEXT,
|
|
407
|
+
quote_owner_id TEXT,
|
|
408
|
+
quote_text TEXT,
|
|
409
|
+
source TEXT NOT NULL,
|
|
410
|
+
raw_message_json TEXT,
|
|
411
|
+
raw_payload_json TEXT,
|
|
412
|
+
created_at TEXT NOT NULL,
|
|
413
|
+
updated_at TEXT NOT NULL,
|
|
414
|
+
PRIMARY KEY (profile, message_uid)
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
CREATE TABLE IF NOT EXISTS message_media (
|
|
418
|
+
profile TEXT NOT NULL,
|
|
419
|
+
message_uid TEXT NOT NULL,
|
|
420
|
+
item_index INTEGER NOT NULL,
|
|
421
|
+
media_kind TEXT,
|
|
422
|
+
media_url TEXT,
|
|
423
|
+
media_path TEXT,
|
|
424
|
+
media_type TEXT,
|
|
425
|
+
raw_json TEXT,
|
|
426
|
+
created_at TEXT NOT NULL,
|
|
427
|
+
updated_at TEXT NOT NULL,
|
|
428
|
+
PRIMARY KEY (profile, message_uid, item_index)
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
432
|
+
profile TEXT NOT NULL,
|
|
433
|
+
message_uid TEXT NOT NULL,
|
|
434
|
+
item_index INTEGER NOT NULL,
|
|
435
|
+
target_user_id TEXT NOT NULL,
|
|
436
|
+
pos INTEGER,
|
|
437
|
+
len INTEGER,
|
|
438
|
+
mention_type INTEGER,
|
|
439
|
+
raw_json TEXT,
|
|
440
|
+
created_at TEXT NOT NULL,
|
|
441
|
+
updated_at TEXT NOT NULL,
|
|
442
|
+
PRIMARY KEY (profile, message_uid, item_index)
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
446
|
+
profile TEXT NOT NULL,
|
|
447
|
+
scope TEXT NOT NULL,
|
|
448
|
+
scope_thread_id TEXT NOT NULL,
|
|
449
|
+
thread_type TEXT NOT NULL,
|
|
450
|
+
status TEXT NOT NULL,
|
|
451
|
+
completeness TEXT,
|
|
452
|
+
cursor TEXT,
|
|
453
|
+
last_sync_at TEXT,
|
|
454
|
+
error TEXT,
|
|
455
|
+
created_at TEXT NOT NULL,
|
|
456
|
+
updated_at TEXT NOT NULL,
|
|
457
|
+
PRIMARY KEY (profile, scope)
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread_time
|
|
461
|
+
ON messages (profile, scope_thread_id, timestamp_ms DESC);
|
|
462
|
+
CREATE INDEX IF NOT EXISTS idx_messages_msg_id
|
|
463
|
+
ON messages (profile, msg_id);
|
|
464
|
+
CREATE INDEX IF NOT EXISTS idx_messages_cli_msg_id
|
|
465
|
+
ON messages (profile, cli_msg_id);
|
|
466
|
+
CREATE INDEX IF NOT EXISTS idx_threads_type
|
|
467
|
+
ON threads (profile, thread_type, updated_at DESC);
|
|
468
|
+
CREATE INDEX IF NOT EXISTS idx_members_thread
|
|
469
|
+
ON thread_members (profile, scope_thread_id);
|
|
470
|
+
CREATE INDEX IF NOT EXISTS idx_friends_name
|
|
471
|
+
ON friends (profile, display_name, zalo_name, user_id);
|
|
472
|
+
`);
|
|
473
|
+
}
|
|
474
|
+
async function openDb(profile) {
|
|
475
|
+
const filename = await resolveDbPath(profile);
|
|
476
|
+
await fs2.mkdir(path2.dirname(filename), { recursive: true });
|
|
477
|
+
const db = await open({
|
|
478
|
+
filename,
|
|
479
|
+
driver: sqlite3.Database
|
|
480
|
+
});
|
|
481
|
+
await migrateDb(db);
|
|
482
|
+
return db;
|
|
483
|
+
}
|
|
484
|
+
async function getDb(profile) {
|
|
485
|
+
const existing = connections.get(profile);
|
|
486
|
+
if (existing) {
|
|
487
|
+
return existing;
|
|
488
|
+
}
|
|
489
|
+
const created = openDb(profile).catch((error) => {
|
|
490
|
+
connections.delete(profile);
|
|
491
|
+
throw error;
|
|
492
|
+
});
|
|
493
|
+
connections.set(profile, created);
|
|
494
|
+
return created;
|
|
495
|
+
}
|
|
496
|
+
async function closeDb(profile) {
|
|
497
|
+
const existing = connections.get(profile);
|
|
498
|
+
if (!existing) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
connections.delete(profile);
|
|
502
|
+
const db = await existing;
|
|
503
|
+
await db.close();
|
|
504
|
+
}
|
|
505
|
+
function resolveDmPeerId(params) {
|
|
506
|
+
const threadId = normalizeId(params.threadId);
|
|
507
|
+
const senderId = normalizeId(params.senderId);
|
|
508
|
+
const toId = normalizeId(params.toId);
|
|
509
|
+
const selfId = normalizeId(params.selfId);
|
|
510
|
+
if (selfId) {
|
|
511
|
+
if (senderId === selfId && toId && toId !== selfId) {
|
|
512
|
+
return toId;
|
|
513
|
+
}
|
|
514
|
+
if (toId === selfId && senderId && senderId !== selfId) {
|
|
515
|
+
return senderId;
|
|
516
|
+
}
|
|
517
|
+
if (threadId && threadId !== selfId) {
|
|
518
|
+
return threadId;
|
|
519
|
+
}
|
|
520
|
+
if (toId && toId !== selfId) {
|
|
521
|
+
return toId;
|
|
522
|
+
}
|
|
523
|
+
if (senderId && senderId !== selfId) {
|
|
524
|
+
return senderId;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (senderId && toId && senderId === threadId && toId !== senderId) {
|
|
528
|
+
return toId;
|
|
529
|
+
}
|
|
530
|
+
if (senderId && toId && toId === threadId && senderId !== toId) {
|
|
531
|
+
return senderId;
|
|
532
|
+
}
|
|
533
|
+
if (threadId) {
|
|
534
|
+
return threadId;
|
|
535
|
+
}
|
|
536
|
+
if (toId && toId !== senderId) {
|
|
537
|
+
return toId;
|
|
538
|
+
}
|
|
539
|
+
return senderId;
|
|
540
|
+
}
|
|
541
|
+
function resolveScopeThreadId(params) {
|
|
542
|
+
if (params.threadType === "group") {
|
|
543
|
+
return normalizeId(params.rawThreadId);
|
|
544
|
+
}
|
|
545
|
+
return resolveDmPeerId({
|
|
546
|
+
threadId: params.rawThreadId,
|
|
547
|
+
senderId: params.senderId,
|
|
548
|
+
toId: params.toId,
|
|
549
|
+
selfId: params.selfId
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function toMessageUid(record) {
|
|
553
|
+
const scopeThreadId = normalizeId(record.scopeThreadId);
|
|
554
|
+
const msgId = normalizeId(record.msgId);
|
|
555
|
+
const cliMsgId = normalizeId(record.cliMsgId);
|
|
556
|
+
const actionId = normalizeId(record.actionId);
|
|
557
|
+
const timestamp = String(record.timestampMs || 0);
|
|
558
|
+
if (msgId) {
|
|
559
|
+
return `${scopeThreadId}:msg:${msgId}`;
|
|
560
|
+
}
|
|
561
|
+
if (cliMsgId) {
|
|
562
|
+
return `${scopeThreadId}:cli:${cliMsgId}`;
|
|
563
|
+
}
|
|
564
|
+
if (actionId) {
|
|
565
|
+
return `${scopeThreadId}:action:${actionId}`;
|
|
566
|
+
}
|
|
567
|
+
const hash = crypto.createHash("sha256").update(
|
|
568
|
+
JSON.stringify({
|
|
569
|
+
scopeThreadId,
|
|
570
|
+
rawThreadId: record.rawThreadId,
|
|
571
|
+
senderId: record.senderId,
|
|
572
|
+
toId: record.toId,
|
|
573
|
+
timestamp,
|
|
574
|
+
msgType: record.msgType,
|
|
575
|
+
contentText: record.contentText
|
|
576
|
+
})
|
|
577
|
+
).digest("hex").slice(0, 24);
|
|
578
|
+
return `${scopeThreadId}:hash:${hash}`;
|
|
579
|
+
}
|
|
580
|
+
async function withWriteQueue(profile, task) {
|
|
581
|
+
const prior = writeQueues.get(profile) ?? Promise.resolve();
|
|
582
|
+
const next = prior.catch(() => void 0).then(task);
|
|
583
|
+
writeQueues.set(profile, next);
|
|
584
|
+
try {
|
|
585
|
+
await next;
|
|
586
|
+
} finally {
|
|
587
|
+
if (writeQueues.get(profile) === next) {
|
|
588
|
+
writeQueues.delete(profile);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function enqueueDbWrite(profile, task) {
|
|
593
|
+
void withWriteQueue(profile, task).catch(() => {
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
async function persistThread(record) {
|
|
597
|
+
const db = await getDb(record.profile);
|
|
598
|
+
const now = nowIso2();
|
|
599
|
+
await db.run(
|
|
600
|
+
`
|
|
601
|
+
INSERT INTO threads (
|
|
602
|
+
profile, scope_thread_id, raw_thread_id, thread_type, peer_id, title,
|
|
603
|
+
is_pinned, is_hidden, is_archived, raw_json, created_at, updated_at
|
|
604
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
605
|
+
ON CONFLICT(profile, scope_thread_id) DO UPDATE SET
|
|
606
|
+
raw_thread_id = excluded.raw_thread_id,
|
|
607
|
+
thread_type = excluded.thread_type,
|
|
608
|
+
peer_id = COALESCE(excluded.peer_id, threads.peer_id),
|
|
609
|
+
title = COALESCE(excluded.title, threads.title),
|
|
610
|
+
is_pinned = excluded.is_pinned,
|
|
611
|
+
is_hidden = excluded.is_hidden,
|
|
612
|
+
is_archived = excluded.is_archived,
|
|
613
|
+
raw_json = COALESCE(excluded.raw_json, threads.raw_json),
|
|
614
|
+
updated_at = excluded.updated_at
|
|
615
|
+
`,
|
|
616
|
+
[
|
|
617
|
+
record.profile,
|
|
618
|
+
record.scopeThreadId,
|
|
619
|
+
record.rawThreadId,
|
|
620
|
+
record.threadType,
|
|
621
|
+
record.peerId ?? null,
|
|
622
|
+
record.title ?? null,
|
|
623
|
+
record.isPinned ? 1 : 0,
|
|
624
|
+
record.isHidden ? 1 : 0,
|
|
625
|
+
record.isArchived ? 1 : 0,
|
|
626
|
+
record.rawJson ?? null,
|
|
627
|
+
now,
|
|
628
|
+
now
|
|
629
|
+
]
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
async function replaceThreadMembers(profile, scopeThreadId, members) {
|
|
633
|
+
const db = await getDb(profile);
|
|
634
|
+
const now = nowIso2();
|
|
635
|
+
await db.exec("BEGIN");
|
|
636
|
+
try {
|
|
637
|
+
await db.run(
|
|
638
|
+
`DELETE FROM thread_members WHERE profile = ? AND scope_thread_id = ?`,
|
|
639
|
+
[profile, scopeThreadId]
|
|
640
|
+
);
|
|
641
|
+
for (const member of members) {
|
|
642
|
+
await db.run(
|
|
643
|
+
`
|
|
644
|
+
INSERT INTO thread_members (
|
|
645
|
+
profile, scope_thread_id, user_id, display_name, zalo_name, avatar,
|
|
646
|
+
account_status, member_type, raw_json, snapshot_at_ms, created_at, updated_at
|
|
647
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
648
|
+
`,
|
|
649
|
+
[
|
|
650
|
+
member.profile,
|
|
651
|
+
member.scopeThreadId,
|
|
652
|
+
member.userId,
|
|
653
|
+
member.displayName ?? null,
|
|
654
|
+
member.zaloName ?? null,
|
|
655
|
+
member.avatar ?? null,
|
|
656
|
+
member.accountStatus ?? null,
|
|
657
|
+
member.memberType ?? null,
|
|
658
|
+
member.rawJson ?? null,
|
|
659
|
+
member.snapshotAtMs,
|
|
660
|
+
now,
|
|
661
|
+
now
|
|
662
|
+
]
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
await db.exec("COMMIT");
|
|
666
|
+
} catch (error) {
|
|
667
|
+
await db.exec("ROLLBACK");
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function persistFriend(record) {
|
|
672
|
+
const db = await getDb(record.profile);
|
|
673
|
+
const now = nowIso2();
|
|
674
|
+
await db.run(
|
|
675
|
+
`
|
|
676
|
+
INSERT INTO friends (
|
|
677
|
+
profile, user_id, display_name, zalo_name, avatar,
|
|
678
|
+
account_status, raw_json, created_at, updated_at
|
|
679
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
680
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
681
|
+
display_name = COALESCE(excluded.display_name, friends.display_name),
|
|
682
|
+
zalo_name = COALESCE(excluded.zalo_name, friends.zalo_name),
|
|
683
|
+
avatar = COALESCE(excluded.avatar, friends.avatar),
|
|
684
|
+
account_status = COALESCE(excluded.account_status, friends.account_status),
|
|
685
|
+
raw_json = COALESCE(excluded.raw_json, friends.raw_json),
|
|
686
|
+
updated_at = excluded.updated_at
|
|
687
|
+
`,
|
|
688
|
+
[
|
|
689
|
+
record.profile,
|
|
690
|
+
record.userId,
|
|
691
|
+
record.displayName ?? null,
|
|
692
|
+
record.zaloName ?? null,
|
|
693
|
+
record.avatar ?? null,
|
|
694
|
+
record.accountStatus ?? null,
|
|
695
|
+
record.rawJson ?? null,
|
|
696
|
+
now,
|
|
697
|
+
now
|
|
698
|
+
]
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
async function persistSelfProfile(params) {
|
|
702
|
+
const db = await getDb(params.profile);
|
|
703
|
+
const now = nowIso2();
|
|
704
|
+
await db.run(
|
|
705
|
+
`
|
|
706
|
+
INSERT INTO self_profiles (
|
|
707
|
+
profile, user_id, display_name, info_json, created_at, updated_at
|
|
708
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
709
|
+
ON CONFLICT(profile) DO UPDATE SET
|
|
710
|
+
user_id = excluded.user_id,
|
|
711
|
+
display_name = COALESCE(excluded.display_name, self_profiles.display_name),
|
|
712
|
+
info_json = COALESCE(excluded.info_json, self_profiles.info_json),
|
|
713
|
+
updated_at = excluded.updated_at
|
|
714
|
+
`,
|
|
715
|
+
[
|
|
716
|
+
params.profile,
|
|
717
|
+
params.userId,
|
|
718
|
+
params.displayName ?? null,
|
|
719
|
+
params.infoJson ?? null,
|
|
720
|
+
now,
|
|
721
|
+
now
|
|
722
|
+
]
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
async function persistMessage(record) {
|
|
726
|
+
const db = await getDb(record.profile);
|
|
727
|
+
const now = nowIso2();
|
|
728
|
+
const messageUid = toMessageUid(record);
|
|
729
|
+
await db.exec("BEGIN");
|
|
730
|
+
try {
|
|
731
|
+
await persistThread({
|
|
732
|
+
profile: record.profile,
|
|
733
|
+
scopeThreadId: record.scopeThreadId,
|
|
734
|
+
rawThreadId: record.rawThreadId,
|
|
735
|
+
threadType: record.threadType,
|
|
736
|
+
peerId: record.peerId,
|
|
737
|
+
title: record.title
|
|
738
|
+
});
|
|
739
|
+
await db.run(
|
|
740
|
+
`
|
|
741
|
+
INSERT INTO messages (
|
|
742
|
+
profile, message_uid, scope_thread_id, raw_thread_id, thread_type,
|
|
743
|
+
msg_id, cli_msg_id, action_id, sender_id, sender_name, to_id,
|
|
744
|
+
timestamp_ms, msg_type, content_text, content_json,
|
|
745
|
+
quote_msg_id, quote_cli_msg_id, quote_owner_id, quote_text,
|
|
746
|
+
source, raw_message_json, raw_payload_json, created_at, updated_at
|
|
747
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
748
|
+
ON CONFLICT(profile, message_uid) DO UPDATE SET
|
|
749
|
+
scope_thread_id = excluded.scope_thread_id,
|
|
750
|
+
raw_thread_id = excluded.raw_thread_id,
|
|
751
|
+
thread_type = excluded.thread_type,
|
|
752
|
+
msg_id = COALESCE(excluded.msg_id, messages.msg_id),
|
|
753
|
+
cli_msg_id = COALESCE(excluded.cli_msg_id, messages.cli_msg_id),
|
|
754
|
+
action_id = COALESCE(excluded.action_id, messages.action_id),
|
|
755
|
+
sender_id = COALESCE(excluded.sender_id, messages.sender_id),
|
|
756
|
+
sender_name = COALESCE(excluded.sender_name, messages.sender_name),
|
|
757
|
+
to_id = COALESCE(excluded.to_id, messages.to_id),
|
|
758
|
+
timestamp_ms = excluded.timestamp_ms,
|
|
759
|
+
msg_type = COALESCE(excluded.msg_type, messages.msg_type),
|
|
760
|
+
content_text = COALESCE(excluded.content_text, messages.content_text),
|
|
761
|
+
content_json = COALESCE(excluded.content_json, messages.content_json),
|
|
762
|
+
quote_msg_id = COALESCE(excluded.quote_msg_id, messages.quote_msg_id),
|
|
763
|
+
quote_cli_msg_id = COALESCE(excluded.quote_cli_msg_id, messages.quote_cli_msg_id),
|
|
764
|
+
quote_owner_id = COALESCE(excluded.quote_owner_id, messages.quote_owner_id),
|
|
765
|
+
quote_text = COALESCE(excluded.quote_text, messages.quote_text),
|
|
766
|
+
source = excluded.source,
|
|
767
|
+
raw_message_json = COALESCE(excluded.raw_message_json, messages.raw_message_json),
|
|
768
|
+
raw_payload_json = COALESCE(excluded.raw_payload_json, messages.raw_payload_json),
|
|
769
|
+
updated_at = excluded.updated_at
|
|
770
|
+
`,
|
|
771
|
+
[
|
|
772
|
+
record.profile,
|
|
773
|
+
messageUid,
|
|
774
|
+
record.scopeThreadId,
|
|
775
|
+
record.rawThreadId,
|
|
776
|
+
record.threadType,
|
|
777
|
+
record.msgId ?? null,
|
|
778
|
+
record.cliMsgId ?? null,
|
|
779
|
+
record.actionId ?? null,
|
|
780
|
+
record.senderId ?? null,
|
|
781
|
+
record.senderName ?? null,
|
|
782
|
+
record.toId ?? null,
|
|
783
|
+
record.timestampMs,
|
|
784
|
+
record.msgType ?? null,
|
|
785
|
+
record.contentText ?? null,
|
|
786
|
+
record.contentJson ?? null,
|
|
787
|
+
record.quoteMsgId ?? null,
|
|
788
|
+
record.quoteCliMsgId ?? null,
|
|
789
|
+
record.quoteOwnerId ?? null,
|
|
790
|
+
record.quoteText ?? null,
|
|
791
|
+
record.source,
|
|
792
|
+
record.rawMessageJson ?? null,
|
|
793
|
+
record.rawPayloadJson ?? null,
|
|
794
|
+
now,
|
|
795
|
+
now
|
|
796
|
+
]
|
|
797
|
+
);
|
|
798
|
+
await db.run(
|
|
799
|
+
`DELETE FROM message_media WHERE profile = ? AND message_uid = ?`,
|
|
800
|
+
[record.profile, messageUid]
|
|
801
|
+
);
|
|
802
|
+
await db.run(
|
|
803
|
+
`DELETE FROM message_mentions WHERE profile = ? AND message_uid = ?`,
|
|
804
|
+
[record.profile, messageUid]
|
|
805
|
+
);
|
|
806
|
+
for (const [index, media] of (record.media ?? []).entries()) {
|
|
807
|
+
await db.run(
|
|
808
|
+
`
|
|
809
|
+
INSERT INTO message_media (
|
|
810
|
+
profile, message_uid, item_index, media_kind, media_url,
|
|
811
|
+
media_path, media_type, raw_json, created_at, updated_at
|
|
812
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
813
|
+
`,
|
|
814
|
+
[
|
|
815
|
+
record.profile,
|
|
816
|
+
messageUid,
|
|
817
|
+
index,
|
|
818
|
+
media.mediaKind ?? null,
|
|
819
|
+
media.mediaUrl ?? null,
|
|
820
|
+
media.mediaPath ?? null,
|
|
821
|
+
media.mediaType ?? null,
|
|
822
|
+
media.rawJson ?? null,
|
|
823
|
+
now,
|
|
824
|
+
now
|
|
825
|
+
]
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
for (const [index, mention] of (record.mentions ?? []).entries()) {
|
|
829
|
+
await db.run(
|
|
830
|
+
`
|
|
831
|
+
INSERT INTO message_mentions (
|
|
832
|
+
profile, message_uid, item_index, target_user_id, pos, len,
|
|
833
|
+
mention_type, raw_json, created_at, updated_at
|
|
834
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
835
|
+
`,
|
|
836
|
+
[
|
|
837
|
+
record.profile,
|
|
838
|
+
messageUid,
|
|
839
|
+
index,
|
|
840
|
+
mention.uid,
|
|
841
|
+
mention.pos ?? null,
|
|
842
|
+
mention.len ?? null,
|
|
843
|
+
mention.type ?? null,
|
|
844
|
+
mention.rawJson ?? null,
|
|
845
|
+
now,
|
|
846
|
+
now
|
|
847
|
+
]
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
await db.exec("COMMIT");
|
|
851
|
+
} catch (error) {
|
|
852
|
+
await db.exec("ROLLBACK");
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async function setSyncState(params) {
|
|
857
|
+
const db = await getDb(params.profile);
|
|
858
|
+
const now = nowIso2();
|
|
859
|
+
const scope = `${params.threadType}:${params.scopeThreadId}`;
|
|
860
|
+
await db.run(
|
|
861
|
+
`
|
|
862
|
+
INSERT INTO sync_state (
|
|
863
|
+
profile, scope, scope_thread_id, thread_type, status, completeness,
|
|
864
|
+
cursor, last_sync_at, error, created_at, updated_at
|
|
865
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
866
|
+
ON CONFLICT(profile, scope) DO UPDATE SET
|
|
867
|
+
status = excluded.status,
|
|
868
|
+
completeness = excluded.completeness,
|
|
869
|
+
cursor = COALESCE(excluded.cursor, sync_state.cursor),
|
|
870
|
+
last_sync_at = excluded.last_sync_at,
|
|
871
|
+
error = excluded.error,
|
|
872
|
+
updated_at = excluded.updated_at
|
|
873
|
+
`,
|
|
874
|
+
[
|
|
875
|
+
params.profile,
|
|
876
|
+
scope,
|
|
877
|
+
params.scopeThreadId,
|
|
878
|
+
params.threadType,
|
|
879
|
+
params.status,
|
|
880
|
+
params.completeness ?? null,
|
|
881
|
+
params.cursor ?? null,
|
|
882
|
+
now,
|
|
883
|
+
params.error ?? null,
|
|
884
|
+
now,
|
|
885
|
+
now
|
|
886
|
+
]
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
async function listRecentMessages(params) {
|
|
890
|
+
const db = await getDb(params.profile);
|
|
891
|
+
const rows = await db.all(
|
|
892
|
+
`
|
|
893
|
+
SELECT
|
|
894
|
+
msg_id,
|
|
895
|
+
cli_msg_id,
|
|
896
|
+
scope_thread_id,
|
|
897
|
+
thread_type,
|
|
898
|
+
sender_id,
|
|
899
|
+
sender_name,
|
|
900
|
+
timestamp_ms,
|
|
901
|
+
msg_type,
|
|
902
|
+
content_text,
|
|
903
|
+
content_json
|
|
904
|
+
FROM messages
|
|
905
|
+
WHERE profile = ? AND scope_thread_id = ? AND thread_type = ?
|
|
906
|
+
ORDER BY timestamp_ms DESC, COALESCE(msg_id, ''), COALESCE(cli_msg_id, '')
|
|
907
|
+
LIMIT ?
|
|
908
|
+
`,
|
|
909
|
+
[params.profile, params.threadId, params.threadType, params.count]
|
|
910
|
+
);
|
|
911
|
+
return rows.map((row) => ({
|
|
912
|
+
msgId: row.msg_id ?? "",
|
|
913
|
+
cliMsgId: row.cli_msg_id ?? "",
|
|
914
|
+
threadId: row.scope_thread_id,
|
|
915
|
+
threadType: row.thread_type,
|
|
916
|
+
senderId: row.sender_id ?? "",
|
|
917
|
+
senderName: row.sender_name ?? "",
|
|
918
|
+
ts: String(row.timestamp_ms),
|
|
919
|
+
msgType: row.msg_type ?? "",
|
|
920
|
+
undo: {
|
|
921
|
+
msgId: row.msg_id ?? "",
|
|
922
|
+
cliMsgId: row.cli_msg_id ?? "",
|
|
923
|
+
threadId: row.scope_thread_id,
|
|
924
|
+
group: row.thread_type === "group"
|
|
925
|
+
},
|
|
926
|
+
content: row.content_text ?? row.content_json ?? ""
|
|
927
|
+
}));
|
|
928
|
+
}
|
|
929
|
+
async function listMessages(params) {
|
|
930
|
+
const db = await getDb(params.profile);
|
|
931
|
+
const order = params.newestFirst ? "DESC" : "ASC";
|
|
932
|
+
const limit = Number.isFinite(params.limit) ? Math.max(Math.trunc(params.limit), 1) : null;
|
|
933
|
+
const rows = await db.all(
|
|
934
|
+
`
|
|
935
|
+
SELECT
|
|
936
|
+
m.raw_thread_id,
|
|
937
|
+
m.thread_type,
|
|
938
|
+
m.msg_id,
|
|
939
|
+
m.cli_msg_id,
|
|
940
|
+
m.sender_id,
|
|
941
|
+
COALESCE(
|
|
942
|
+
NULLIF(m.sender_name, ''),
|
|
943
|
+
NULLIF(tm.display_name, ''),
|
|
944
|
+
NULLIF(tm.zalo_name, ''),
|
|
945
|
+
NULLIF(f.display_name, ''),
|
|
946
|
+
NULLIF(f.zalo_name, '')
|
|
947
|
+
) AS sender_name,
|
|
948
|
+
m.to_id,
|
|
949
|
+
m.timestamp_ms,
|
|
950
|
+
m.msg_type,
|
|
951
|
+
m.content_text,
|
|
952
|
+
m.content_json,
|
|
953
|
+
m.quote_msg_id,
|
|
954
|
+
m.quote_cli_msg_id,
|
|
955
|
+
m.quote_owner_id,
|
|
956
|
+
m.quote_text,
|
|
957
|
+
m.source
|
|
958
|
+
FROM messages m
|
|
959
|
+
LEFT JOIN thread_members tm
|
|
960
|
+
ON tm.profile = m.profile
|
|
961
|
+
AND tm.scope_thread_id = m.scope_thread_id
|
|
962
|
+
AND tm.user_id = m.sender_id
|
|
963
|
+
LEFT JOIN friends f
|
|
964
|
+
ON f.profile = m.profile
|
|
965
|
+
AND f.user_id = m.sender_id
|
|
966
|
+
WHERE m.profile = ?
|
|
967
|
+
AND m.scope_thread_id = ?
|
|
968
|
+
AND m.thread_type = ?
|
|
969
|
+
AND (? IS NULL OR timestamp_ms >= ?)
|
|
970
|
+
AND (? IS NULL OR timestamp_ms < ?)
|
|
971
|
+
ORDER BY m.timestamp_ms ${order}, COALESCE(m.msg_id, ''), COALESCE(m.cli_msg_id, '')
|
|
972
|
+
${limit ? "LIMIT ?" : ""}
|
|
973
|
+
`,
|
|
974
|
+
limit ? [
|
|
975
|
+
params.profile,
|
|
976
|
+
params.threadId,
|
|
977
|
+
params.threadType,
|
|
978
|
+
params.sinceMs ?? null,
|
|
979
|
+
params.sinceMs ?? null,
|
|
980
|
+
params.untilMs ?? null,
|
|
981
|
+
params.untilMs ?? null,
|
|
982
|
+
limit
|
|
983
|
+
] : [
|
|
984
|
+
params.profile,
|
|
985
|
+
params.threadId,
|
|
986
|
+
params.threadType,
|
|
987
|
+
params.sinceMs ?? null,
|
|
988
|
+
params.sinceMs ?? null,
|
|
989
|
+
params.untilMs ?? null,
|
|
990
|
+
params.untilMs ?? null
|
|
991
|
+
]
|
|
992
|
+
);
|
|
993
|
+
return rows.map((row) => ({
|
|
994
|
+
msgId: row.msg_id ?? "",
|
|
995
|
+
cliMsgId: row.cli_msg_id ?? "",
|
|
996
|
+
threadId: params.threadId,
|
|
997
|
+
threadType: row.thread_type,
|
|
998
|
+
senderId: row.sender_id ?? "",
|
|
999
|
+
senderName: row.sender_name ?? "",
|
|
1000
|
+
ts: String(row.timestamp_ms),
|
|
1001
|
+
timestampMs: row.timestamp_ms,
|
|
1002
|
+
msgType: row.msg_type ?? "",
|
|
1003
|
+
undo: {
|
|
1004
|
+
msgId: row.msg_id ?? "",
|
|
1005
|
+
cliMsgId: row.cli_msg_id ?? "",
|
|
1006
|
+
threadId: params.threadId,
|
|
1007
|
+
group: row.thread_type === "group"
|
|
1008
|
+
},
|
|
1009
|
+
content: row.content_text ?? row.content_json ?? "",
|
|
1010
|
+
rawThreadId: row.raw_thread_id,
|
|
1011
|
+
toId: row.to_id ?? void 0,
|
|
1012
|
+
quoteMsgId: row.quote_msg_id ?? void 0,
|
|
1013
|
+
quoteCliMsgId: row.quote_cli_msg_id ?? void 0,
|
|
1014
|
+
quoteOwnerId: row.quote_owner_id ?? void 0,
|
|
1015
|
+
quoteText: row.quote_text ?? void 0,
|
|
1016
|
+
source: row.source
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
async function getMessageById(params) {
|
|
1020
|
+
const db = await getDb(params.profile);
|
|
1021
|
+
const row = await db.get(
|
|
1022
|
+
`
|
|
1023
|
+
SELECT *
|
|
1024
|
+
FROM messages
|
|
1025
|
+
WHERE profile = ?
|
|
1026
|
+
AND (
|
|
1027
|
+
msg_id = ?
|
|
1028
|
+
OR cli_msg_id = ?
|
|
1029
|
+
OR message_uid = ?
|
|
1030
|
+
)
|
|
1031
|
+
ORDER BY timestamp_ms DESC
|
|
1032
|
+
LIMIT 1
|
|
1033
|
+
`,
|
|
1034
|
+
[params.profile, params.id, params.id, params.id]
|
|
1035
|
+
);
|
|
1036
|
+
if (!row) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
threadId: row.scope_thread_id,
|
|
1041
|
+
rawThreadId: row.raw_thread_id,
|
|
1042
|
+
threadType: row.thread_type,
|
|
1043
|
+
msgId: row.msg_id ?? void 0,
|
|
1044
|
+
cliMsgId: row.cli_msg_id ?? void 0,
|
|
1045
|
+
actionId: row.action_id ?? void 0,
|
|
1046
|
+
senderId: row.sender_id ?? void 0,
|
|
1047
|
+
senderName: row.sender_name ?? void 0,
|
|
1048
|
+
toId: row.to_id ?? void 0,
|
|
1049
|
+
timestampMs: row.timestamp_ms,
|
|
1050
|
+
msgType: row.msg_type ?? void 0,
|
|
1051
|
+
content: row.content_text ?? void 0,
|
|
1052
|
+
contentJson: row.content_json ?? void 0,
|
|
1053
|
+
quoteMsgId: row.quote_msg_id ?? void 0,
|
|
1054
|
+
quoteCliMsgId: row.quote_cli_msg_id ?? void 0,
|
|
1055
|
+
quoteOwnerId: row.quote_owner_id ?? void 0,
|
|
1056
|
+
quoteText: row.quote_text ?? void 0,
|
|
1057
|
+
source: row.source,
|
|
1058
|
+
rawMessage: row.raw_message_json ? JSON.parse(row.raw_message_json) : void 0,
|
|
1059
|
+
rawPayload: row.raw_payload_json ? JSON.parse(row.raw_payload_json) : void 0
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
async function listThreads(params) {
|
|
1063
|
+
const db = await getDb(params.profile);
|
|
1064
|
+
const rows = await db.all(
|
|
1065
|
+
`
|
|
1066
|
+
SELECT
|
|
1067
|
+
t.scope_thread_id,
|
|
1068
|
+
t.raw_thread_id,
|
|
1069
|
+
t.thread_type,
|
|
1070
|
+
t.title,
|
|
1071
|
+
t.peer_id,
|
|
1072
|
+
t.is_pinned,
|
|
1073
|
+
t.is_hidden,
|
|
1074
|
+
t.is_archived,
|
|
1075
|
+
COUNT(m.message_uid) AS message_count,
|
|
1076
|
+
MIN(m.timestamp_ms) AS first_message_at_ms,
|
|
1077
|
+
MAX(m.timestamp_ms) AS last_message_at_ms,
|
|
1078
|
+
(
|
|
1079
|
+
SELECT COUNT(*)
|
|
1080
|
+
FROM thread_members tm
|
|
1081
|
+
WHERE tm.profile = t.profile AND tm.scope_thread_id = t.scope_thread_id
|
|
1082
|
+
) AS member_count
|
|
1083
|
+
FROM threads t
|
|
1084
|
+
LEFT JOIN messages m
|
|
1085
|
+
ON m.profile = t.profile AND m.scope_thread_id = t.scope_thread_id
|
|
1086
|
+
WHERE t.profile = ?
|
|
1087
|
+
AND (? IS NULL OR t.thread_type = ?)
|
|
1088
|
+
GROUP BY
|
|
1089
|
+
t.scope_thread_id, t.raw_thread_id, t.thread_type, t.title, t.peer_id,
|
|
1090
|
+
t.is_pinned, t.is_hidden, t.is_archived
|
|
1091
|
+
ORDER BY COALESCE(MAX(m.timestamp_ms), 0) DESC, t.scope_thread_id
|
|
1092
|
+
`,
|
|
1093
|
+
[params.profile, params.threadType ?? null, params.threadType ?? null]
|
|
1094
|
+
);
|
|
1095
|
+
return rows.map((row) => ({
|
|
1096
|
+
threadId: row.scope_thread_id,
|
|
1097
|
+
rawThreadId: row.raw_thread_id,
|
|
1098
|
+
threadType: row.thread_type,
|
|
1099
|
+
title: row.title ?? void 0,
|
|
1100
|
+
peerId: row.peer_id ?? void 0,
|
|
1101
|
+
messageCount: row.message_count,
|
|
1102
|
+
firstMessageAtMs: row.first_message_at_ms ?? void 0,
|
|
1103
|
+
lastMessageAtMs: row.last_message_at_ms ?? void 0,
|
|
1104
|
+
memberCount: row.member_count,
|
|
1105
|
+
isPinned: row.is_pinned === 1,
|
|
1106
|
+
isHidden: row.is_hidden === 1,
|
|
1107
|
+
isArchived: row.is_archived === 1
|
|
1108
|
+
}));
|
|
1109
|
+
}
|
|
1110
|
+
async function listGroups(profile) {
|
|
1111
|
+
return listThreads({ profile, threadType: "group" });
|
|
1112
|
+
}
|
|
1113
|
+
async function getThreadInfo(params) {
|
|
1114
|
+
const db = await getDb(params.profile);
|
|
1115
|
+
const row = await db.get(
|
|
1116
|
+
`
|
|
1117
|
+
SELECT
|
|
1118
|
+
t.scope_thread_id,
|
|
1119
|
+
t.raw_thread_id,
|
|
1120
|
+
t.thread_type,
|
|
1121
|
+
t.title,
|
|
1122
|
+
t.peer_id,
|
|
1123
|
+
t.is_pinned,
|
|
1124
|
+
t.is_hidden,
|
|
1125
|
+
t.is_archived,
|
|
1126
|
+
t.raw_json,
|
|
1127
|
+
COUNT(m.message_uid) AS message_count,
|
|
1128
|
+
MIN(m.timestamp_ms) AS first_message_at_ms,
|
|
1129
|
+
MAX(m.timestamp_ms) AS last_message_at_ms,
|
|
1130
|
+
(
|
|
1131
|
+
SELECT COUNT(*)
|
|
1132
|
+
FROM thread_members tm
|
|
1133
|
+
WHERE tm.profile = t.profile AND tm.scope_thread_id = t.scope_thread_id
|
|
1134
|
+
) AS member_count
|
|
1135
|
+
FROM threads t
|
|
1136
|
+
LEFT JOIN messages m
|
|
1137
|
+
ON m.profile = t.profile AND m.scope_thread_id = t.scope_thread_id
|
|
1138
|
+
WHERE t.profile = ?
|
|
1139
|
+
AND (? IS NULL OR t.thread_type = ?)
|
|
1140
|
+
AND (t.scope_thread_id = ? OR t.raw_thread_id = ?)
|
|
1141
|
+
GROUP BY
|
|
1142
|
+
t.scope_thread_id, t.raw_thread_id, t.thread_type, t.title, t.peer_id,
|
|
1143
|
+
t.is_pinned, t.is_hidden, t.is_archived, t.raw_json
|
|
1144
|
+
ORDER BY
|
|
1145
|
+
CASE WHEN t.scope_thread_id = ? THEN 0 ELSE 1 END,
|
|
1146
|
+
COALESCE(MAX(m.timestamp_ms), 0) DESC
|
|
1147
|
+
LIMIT 1
|
|
1148
|
+
`,
|
|
1149
|
+
[
|
|
1150
|
+
params.profile,
|
|
1151
|
+
params.threadType ?? null,
|
|
1152
|
+
params.threadType ?? null,
|
|
1153
|
+
params.threadId,
|
|
1154
|
+
params.threadId,
|
|
1155
|
+
params.threadId
|
|
1156
|
+
]
|
|
1157
|
+
);
|
|
1158
|
+
if (!row) {
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
threadId: row.scope_thread_id,
|
|
1163
|
+
rawThreadId: row.raw_thread_id,
|
|
1164
|
+
threadType: row.thread_type,
|
|
1165
|
+
title: row.title ?? void 0,
|
|
1166
|
+
peerId: row.peer_id ?? void 0,
|
|
1167
|
+
messageCount: row.message_count,
|
|
1168
|
+
firstMessageAtMs: row.first_message_at_ms ?? void 0,
|
|
1169
|
+
lastMessageAtMs: row.last_message_at_ms ?? void 0,
|
|
1170
|
+
memberCount: row.member_count,
|
|
1171
|
+
isPinned: row.is_pinned === 1,
|
|
1172
|
+
isHidden: row.is_hidden === 1,
|
|
1173
|
+
isArchived: row.is_archived === 1,
|
|
1174
|
+
raw: row.raw_json ? JSON.parse(row.raw_json) : void 0
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
async function listFriends(profile) {
|
|
1178
|
+
const db = await getDb(profile);
|
|
1179
|
+
const rows = await db.all(
|
|
1180
|
+
`
|
|
1181
|
+
SELECT
|
|
1182
|
+
f.user_id,
|
|
1183
|
+
f.display_name,
|
|
1184
|
+
f.zalo_name,
|
|
1185
|
+
f.avatar,
|
|
1186
|
+
f.account_status,
|
|
1187
|
+
t.scope_thread_id AS chat_id,
|
|
1188
|
+
t.title,
|
|
1189
|
+
COUNT(m.message_uid) AS message_count,
|
|
1190
|
+
MAX(m.timestamp_ms) AS last_message_at_ms
|
|
1191
|
+
FROM friends f
|
|
1192
|
+
LEFT JOIN threads t
|
|
1193
|
+
ON t.profile = f.profile
|
|
1194
|
+
AND t.thread_type = 'user'
|
|
1195
|
+
AND (t.peer_id = f.user_id OR t.scope_thread_id = f.user_id OR t.raw_thread_id = f.user_id)
|
|
1196
|
+
LEFT JOIN messages m
|
|
1197
|
+
ON m.profile = t.profile
|
|
1198
|
+
AND m.scope_thread_id = t.scope_thread_id
|
|
1199
|
+
WHERE f.profile = ?
|
|
1200
|
+
GROUP BY
|
|
1201
|
+
f.user_id, f.display_name, f.zalo_name, f.avatar, f.account_status, t.scope_thread_id, t.title
|
|
1202
|
+
ORDER BY COALESCE(f.display_name, f.zalo_name, f.user_id), f.user_id
|
|
1203
|
+
`,
|
|
1204
|
+
[profile]
|
|
1205
|
+
);
|
|
1206
|
+
return rows.map((row) => ({
|
|
1207
|
+
userId: row.user_id,
|
|
1208
|
+
displayName: row.display_name ?? void 0,
|
|
1209
|
+
zaloName: row.zalo_name ?? void 0,
|
|
1210
|
+
avatar: row.avatar ?? void 0,
|
|
1211
|
+
accountStatus: row.account_status ?? void 0,
|
|
1212
|
+
title: row.title ?? void 0,
|
|
1213
|
+
chatId: row.chat_id ?? row.user_id,
|
|
1214
|
+
messageCount: row.message_count,
|
|
1215
|
+
lastMessageAtMs: row.last_message_at_ms ?? void 0
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
1218
|
+
async function getFriendInfo(params) {
|
|
1219
|
+
const db = await getDb(params.profile);
|
|
1220
|
+
const row = await db.get(
|
|
1221
|
+
`
|
|
1222
|
+
SELECT
|
|
1223
|
+
f.user_id,
|
|
1224
|
+
f.display_name,
|
|
1225
|
+
f.zalo_name,
|
|
1226
|
+
f.avatar,
|
|
1227
|
+
f.account_status,
|
|
1228
|
+
f.raw_json,
|
|
1229
|
+
t.scope_thread_id AS chat_id,
|
|
1230
|
+
t.title,
|
|
1231
|
+
COUNT(m.message_uid) AS message_count,
|
|
1232
|
+
MAX(m.timestamp_ms) AS last_message_at_ms
|
|
1233
|
+
FROM friends f
|
|
1234
|
+
LEFT JOIN threads t
|
|
1235
|
+
ON t.profile = f.profile
|
|
1236
|
+
AND t.thread_type = 'user'
|
|
1237
|
+
AND (t.peer_id = f.user_id OR t.scope_thread_id = f.user_id OR t.raw_thread_id = f.user_id)
|
|
1238
|
+
LEFT JOIN messages m
|
|
1239
|
+
ON m.profile = t.profile
|
|
1240
|
+
AND m.scope_thread_id = t.scope_thread_id
|
|
1241
|
+
WHERE f.profile = ?
|
|
1242
|
+
AND f.user_id = ?
|
|
1243
|
+
GROUP BY
|
|
1244
|
+
f.user_id, f.display_name, f.zalo_name, f.avatar, f.account_status,
|
|
1245
|
+
f.raw_json, t.scope_thread_id, t.title
|
|
1246
|
+
LIMIT 1
|
|
1247
|
+
`,
|
|
1248
|
+
[params.profile, params.userId]
|
|
1249
|
+
);
|
|
1250
|
+
if (!row) {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
userId: row.user_id,
|
|
1255
|
+
displayName: row.display_name ?? void 0,
|
|
1256
|
+
zaloName: row.zalo_name ?? void 0,
|
|
1257
|
+
avatar: row.avatar ?? void 0,
|
|
1258
|
+
accountStatus: row.account_status ?? void 0,
|
|
1259
|
+
title: row.title ?? void 0,
|
|
1260
|
+
chatId: row.chat_id ?? row.user_id,
|
|
1261
|
+
messageCount: row.message_count,
|
|
1262
|
+
lastMessageAtMs: row.last_message_at_ms ?? void 0,
|
|
1263
|
+
raw: row.raw_json ? JSON.parse(row.raw_json) : void 0
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function findFriends(params) {
|
|
1267
|
+
const query = normalizeSearchText(params.query);
|
|
1268
|
+
if (!query) {
|
|
1269
|
+
return [];
|
|
1270
|
+
}
|
|
1271
|
+
const rows = await listFriends(params.profile);
|
|
1272
|
+
return rows.filter((row) => {
|
|
1273
|
+
const haystacks = [
|
|
1274
|
+
row.userId,
|
|
1275
|
+
row.displayName ?? "",
|
|
1276
|
+
row.zaloName ?? "",
|
|
1277
|
+
row.title ?? ""
|
|
1278
|
+
];
|
|
1279
|
+
return haystacks.some((value) => matchesSearchPattern(value, query));
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
async function listChats(profile) {
|
|
1283
|
+
return listThreads({ profile });
|
|
1284
|
+
}
|
|
1285
|
+
async function getSelfProfile(profile) {
|
|
1286
|
+
const db = await getDb(profile);
|
|
1287
|
+
const row = await db.get(
|
|
1288
|
+
`
|
|
1289
|
+
SELECT user_id, display_name, info_json
|
|
1290
|
+
FROM self_profiles
|
|
1291
|
+
WHERE profile = ?
|
|
1292
|
+
LIMIT 1
|
|
1293
|
+
`,
|
|
1294
|
+
[profile]
|
|
1295
|
+
);
|
|
1296
|
+
if (!row) {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
return {
|
|
1300
|
+
userId: row.user_id,
|
|
1301
|
+
displayName: row.display_name ?? void 0,
|
|
1302
|
+
info: row.info_json ? JSON.parse(row.info_json) : void 0
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
async function listThreadMembers(params) {
|
|
1306
|
+
const db = await getDb(params.profile);
|
|
1307
|
+
const rows = await db.all(
|
|
1308
|
+
`
|
|
1309
|
+
SELECT
|
|
1310
|
+
user_id, display_name, zalo_name, avatar, account_status, member_type, snapshot_at_ms
|
|
1311
|
+
FROM thread_members
|
|
1312
|
+
WHERE profile = ? AND scope_thread_id = ?
|
|
1313
|
+
ORDER BY COALESCE(display_name, zalo_name, user_id), user_id
|
|
1314
|
+
`,
|
|
1315
|
+
[params.profile, params.threadId]
|
|
1316
|
+
);
|
|
1317
|
+
return rows.map((row) => ({
|
|
1318
|
+
userId: row.user_id,
|
|
1319
|
+
displayName: row.display_name ?? void 0,
|
|
1320
|
+
zaloName: row.zalo_name ?? void 0,
|
|
1321
|
+
avatar: row.avatar ?? void 0,
|
|
1322
|
+
accountStatus: row.account_status ?? void 0,
|
|
1323
|
+
type: row.member_type ?? void 0,
|
|
1324
|
+
snapshotAtMs: row.snapshot_at_ms
|
|
1325
|
+
}));
|
|
1326
|
+
}
|
|
1327
|
+
async function getDbStatus(profile) {
|
|
1328
|
+
const filename = await resolveDbPath(profile);
|
|
1329
|
+
const exists = await fs2.access(filename).then(() => true).catch(() => false);
|
|
1330
|
+
const config = await readDbConfig(profile);
|
|
1331
|
+
if (!exists) {
|
|
1332
|
+
return {
|
|
1333
|
+
enabled: config.enabled,
|
|
1334
|
+
path: filename,
|
|
1335
|
+
exists: false,
|
|
1336
|
+
messageCount: 0,
|
|
1337
|
+
threadCount: 0,
|
|
1338
|
+
groupCount: 0,
|
|
1339
|
+
userCount: 0,
|
|
1340
|
+
updatedAt: config.updatedAt
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const db = await getDb(profile);
|
|
1344
|
+
const row = await db.get(`
|
|
1345
|
+
SELECT
|
|
1346
|
+
(SELECT COUNT(*) FROM messages WHERE profile = ?) AS message_count,
|
|
1347
|
+
(SELECT COUNT(*) FROM threads WHERE profile = ?) AS thread_count,
|
|
1348
|
+
(SELECT COUNT(*) FROM threads WHERE profile = ? AND thread_type = 'group') AS group_count,
|
|
1349
|
+
(SELECT COUNT(*) FROM threads WHERE profile = ? AND thread_type = 'user') AS user_count,
|
|
1350
|
+
(SELECT MAX(timestamp_ms) FROM messages WHERE profile = ?) AS last_message_at_ms,
|
|
1351
|
+
(
|
|
1352
|
+
SELECT MAX(updated_at)
|
|
1353
|
+
FROM (
|
|
1354
|
+
SELECT updated_at FROM messages WHERE profile = ?
|
|
1355
|
+
UNION ALL
|
|
1356
|
+
SELECT updated_at FROM threads WHERE profile = ?
|
|
1357
|
+
)
|
|
1358
|
+
) AS last_updated_at
|
|
1359
|
+
`, [profile, profile, profile, profile, profile, profile, profile]);
|
|
1360
|
+
return {
|
|
1361
|
+
enabled: config.enabled,
|
|
1362
|
+
path: filename,
|
|
1363
|
+
exists: true,
|
|
1364
|
+
messageCount: row?.message_count ?? 0,
|
|
1365
|
+
threadCount: row?.thread_count ?? 0,
|
|
1366
|
+
groupCount: row?.group_count ?? 0,
|
|
1367
|
+
userCount: row?.user_count ?? 0,
|
|
1368
|
+
lastMessageAtMs: row?.last_message_at_ms ?? void 0,
|
|
1369
|
+
updatedAt: row?.last_updated_at ?? config.updatedAt
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
async function listSyncState(params) {
|
|
1373
|
+
const filename = await resolveDbPath(params.profile);
|
|
1374
|
+
const exists = await fs2.access(filename).then(() => true).catch(() => false);
|
|
1375
|
+
if (!exists) {
|
|
1376
|
+
return [];
|
|
1377
|
+
}
|
|
1378
|
+
const db = await getDb(params.profile);
|
|
1379
|
+
const rows = await db.all(
|
|
1380
|
+
`
|
|
1381
|
+
SELECT scope, scope_thread_id, thread_type, status, cursor, completeness, last_sync_at, error
|
|
1382
|
+
FROM sync_state
|
|
1383
|
+
WHERE profile = ? AND (? IS NULL OR thread_type = ?)
|
|
1384
|
+
ORDER BY COALESCE(last_sync_at, ''), scope
|
|
1385
|
+
`,
|
|
1386
|
+
[params.profile, params.threadType ?? null, params.threadType ?? null]
|
|
1387
|
+
);
|
|
1388
|
+
return rows.map((row) => ({
|
|
1389
|
+
scope: row.scope,
|
|
1390
|
+
scopeThreadId: row.scope_thread_id,
|
|
1391
|
+
threadType: row.thread_type,
|
|
1392
|
+
status: row.status,
|
|
1393
|
+
cursor: row.cursor ?? void 0,
|
|
1394
|
+
completeness: row.completeness ?? void 0,
|
|
1395
|
+
lastSyncAt: row.last_sync_at ?? void 0,
|
|
1396
|
+
error: row.error ?? void 0
|
|
1397
|
+
}));
|
|
1398
|
+
}
|
|
1399
|
+
function normalizeInboundListenRecord(params) {
|
|
1400
|
+
const scopeThreadId = resolveScopeThreadId({
|
|
1401
|
+
threadType: params.threadType,
|
|
1402
|
+
rawThreadId: params.rawThreadId,
|
|
1403
|
+
senderId: params.senderId,
|
|
1404
|
+
toId: params.toId,
|
|
1405
|
+
selfId: params.selfId
|
|
1406
|
+
});
|
|
1407
|
+
return {
|
|
1408
|
+
profile: params.profile,
|
|
1409
|
+
scopeThreadId,
|
|
1410
|
+
rawThreadId: params.rawThreadId,
|
|
1411
|
+
threadType: params.threadType,
|
|
1412
|
+
peerId: params.threadType === "user" ? scopeThreadId : void 0,
|
|
1413
|
+
title: params.title,
|
|
1414
|
+
msgId: normalizeOptionalText(params.msgId),
|
|
1415
|
+
cliMsgId: normalizeOptionalText(params.cliMsgId),
|
|
1416
|
+
actionId: normalizeOptionalText(params.actionId),
|
|
1417
|
+
senderId: normalizeOptionalText(params.senderId),
|
|
1418
|
+
senderName: normalizeOptionalText(params.senderName),
|
|
1419
|
+
toId: normalizeOptionalText(params.toId),
|
|
1420
|
+
timestampMs: params.timestampMs,
|
|
1421
|
+
msgType: normalizeOptionalText(params.msgType),
|
|
1422
|
+
contentText: params.contentText,
|
|
1423
|
+
contentJson: params.contentJson,
|
|
1424
|
+
quoteMsgId: normalizeOptionalText(params.quoteMsgId),
|
|
1425
|
+
quoteCliMsgId: normalizeOptionalText(params.quoteCliMsgId),
|
|
1426
|
+
quoteOwnerId: normalizeOptionalText(params.quoteOwnerId),
|
|
1427
|
+
quoteText: params.quoteText,
|
|
1428
|
+
media: params.media,
|
|
1429
|
+
mentions: params.mentions,
|
|
1430
|
+
source: params.source,
|
|
1431
|
+
rawMessageJson: safeJsonStringify(params.rawMessage),
|
|
1432
|
+
rawPayloadJson: safeJsonStringify(params.rawPayload)
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/lib/client.ts
|
|
1437
|
+
import fs3 from "fs/promises";
|
|
1438
|
+
import { spawn } from "child_process";
|
|
1439
|
+
import path3 from "path";
|
|
217
1440
|
import { imageSize } from "image-size";
|
|
218
1441
|
import * as qrcodeTerminal from "qrcode-terminal";
|
|
219
1442
|
import {
|
|
@@ -248,7 +1471,7 @@ function renderInlineQrPngIfSupported(pngBase64, filePath) {
|
|
|
248
1471
|
if (debug) console.error("[openzca] QR render mode: iterm");
|
|
249
1472
|
const widthEnv = Number.parseInt(process.env.OPENZCA_QR_WIDTH ?? "", 10);
|
|
250
1473
|
const widthCells = Number.isFinite(widthEnv) && widthEnv >= 16 && widthEnv <= 80 ? widthEnv : 34;
|
|
251
|
-
const encodedName = Buffer.from(
|
|
1474
|
+
const encodedName = Buffer.from(path3.basename(filePath)).toString("base64");
|
|
252
1475
|
const osc1337 = `\x1B]1337;File=name=${encodedName};inline=1;width=${widthCells};preserveAspectRatio=1:${pngBase64}\x07`;
|
|
253
1476
|
process.stdout.write(`${osc1337}
|
|
254
1477
|
`);
|
|
@@ -321,7 +1544,7 @@ function tryOpenFile(filePath) {
|
|
|
321
1544
|
}
|
|
322
1545
|
}
|
|
323
1546
|
async function imageMetadataGetter(filePath) {
|
|
324
|
-
const data = await
|
|
1547
|
+
const data = await fs3.readFile(filePath);
|
|
325
1548
|
const info = imageSize(data);
|
|
326
1549
|
if (!info.width || !info.height) {
|
|
327
1550
|
throw new Error(`Cannot read image size: ${filePath}`);
|
|
@@ -376,7 +1599,7 @@ async function loginWithQrAndPersist(profileName, qrPath, opts) {
|
|
|
376
1599
|
console.log("\nScan this QR in your Zalo app:\n");
|
|
377
1600
|
const targetPath = qrPath ?? "qr.png";
|
|
378
1601
|
await event.actions.saveToFile(targetPath);
|
|
379
|
-
const absolutePath =
|
|
1602
|
+
const absolutePath = path3.resolve(targetPath);
|
|
380
1603
|
const rendered = renderInlineQrPngIfSupported(
|
|
381
1604
|
event.data.image,
|
|
382
1605
|
targetPath
|
|
@@ -452,9 +1675,9 @@ async function loginWithQrAndPersist(profileName, qrPath, opts) {
|
|
|
452
1675
|
}
|
|
453
1676
|
|
|
454
1677
|
// src/lib/media.ts
|
|
455
|
-
import
|
|
1678
|
+
import fs4 from "fs/promises";
|
|
456
1679
|
import os2 from "os";
|
|
457
|
-
import
|
|
1680
|
+
import path4 from "path";
|
|
458
1681
|
import { fileURLToPath } from "url";
|
|
459
1682
|
var CONTENT_TYPE_EXT = {
|
|
460
1683
|
"image/jpeg": ".jpg",
|
|
@@ -502,7 +1725,7 @@ function expandLeadingTilde(value) {
|
|
|
502
1725
|
return os2.homedir();
|
|
503
1726
|
}
|
|
504
1727
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
505
|
-
return
|
|
1728
|
+
return path4.join(os2.homedir(), value.slice(2));
|
|
506
1729
|
}
|
|
507
1730
|
return value;
|
|
508
1731
|
}
|
|
@@ -534,7 +1757,7 @@ function inferExt(url, contentType) {
|
|
|
534
1757
|
}
|
|
535
1758
|
try {
|
|
536
1759
|
const pathname = new URL(url).pathname;
|
|
537
|
-
const ext =
|
|
1760
|
+
const ext = path4.extname(pathname);
|
|
538
1761
|
if (ext) return ext;
|
|
539
1762
|
} catch {
|
|
540
1763
|
}
|
|
@@ -547,7 +1770,7 @@ async function downloadUrlsToTempFiles(urls) {
|
|
|
547
1770
|
cleanup: async () => Promise.resolve()
|
|
548
1771
|
};
|
|
549
1772
|
}
|
|
550
|
-
const dir = await
|
|
1773
|
+
const dir = await fs4.mkdtemp(path4.join(os2.tmpdir(), "openzca-"));
|
|
551
1774
|
const files = [];
|
|
552
1775
|
for (let i = 0; i < urls.length; i += 1) {
|
|
553
1776
|
const url = urls[i];
|
|
@@ -556,22 +1779,22 @@ async function downloadUrlsToTempFiles(urls) {
|
|
|
556
1779
|
throw new Error(`Failed to download URL: ${url} (${response.status})`);
|
|
557
1780
|
}
|
|
558
1781
|
const ext = inferExt(url, response.headers.get("content-type"));
|
|
559
|
-
const filePath =
|
|
1782
|
+
const filePath = path4.join(dir, `url-${i + 1}${ext}`);
|
|
560
1783
|
const data = Buffer.from(await response.arrayBuffer());
|
|
561
|
-
await
|
|
1784
|
+
await fs4.writeFile(filePath, data);
|
|
562
1785
|
files.push(filePath);
|
|
563
1786
|
}
|
|
564
1787
|
return {
|
|
565
1788
|
files,
|
|
566
1789
|
cleanup: async () => {
|
|
567
|
-
await
|
|
1790
|
+
await fs4.rm(dir, { recursive: true, force: true });
|
|
568
1791
|
}
|
|
569
1792
|
};
|
|
570
1793
|
}
|
|
571
1794
|
async function assertFilesExist(files) {
|
|
572
1795
|
for (const file of files) {
|
|
573
1796
|
try {
|
|
574
|
-
await
|
|
1797
|
+
await fs4.access(file);
|
|
575
1798
|
} catch {
|
|
576
1799
|
throw new Error(`File not found: ${file}`);
|
|
577
1800
|
}
|
|
@@ -632,6 +1855,82 @@ function buildCreatePollOptions(options) {
|
|
|
632
1855
|
};
|
|
633
1856
|
}
|
|
634
1857
|
|
|
1858
|
+
// src/lib/time-range.ts
|
|
1859
|
+
var DURATION_PART_RE = /(\d+)\s*(ms|s|m|h|d|w)/gi;
|
|
1860
|
+
function durationToMs(input) {
|
|
1861
|
+
const text = input.trim().toLowerCase();
|
|
1862
|
+
if (!text) {
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
let total = 0;
|
|
1866
|
+
let matched = 0;
|
|
1867
|
+
for (const match of text.matchAll(DURATION_PART_RE)) {
|
|
1868
|
+
const rawAmount = match[1];
|
|
1869
|
+
const unit = match[2];
|
|
1870
|
+
const amount = Number(rawAmount);
|
|
1871
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1874
|
+
matched += match[0].length;
|
|
1875
|
+
switch (unit) {
|
|
1876
|
+
case "ms":
|
|
1877
|
+
total += amount;
|
|
1878
|
+
break;
|
|
1879
|
+
case "s":
|
|
1880
|
+
total += amount * 1e3;
|
|
1881
|
+
break;
|
|
1882
|
+
case "m":
|
|
1883
|
+
total += amount * 60 * 1e3;
|
|
1884
|
+
break;
|
|
1885
|
+
case "h":
|
|
1886
|
+
total += amount * 60 * 60 * 1e3;
|
|
1887
|
+
break;
|
|
1888
|
+
case "d":
|
|
1889
|
+
total += amount * 24 * 60 * 60 * 1e3;
|
|
1890
|
+
break;
|
|
1891
|
+
case "w":
|
|
1892
|
+
total += amount * 7 * 24 * 60 * 60 * 1e3;
|
|
1893
|
+
break;
|
|
1894
|
+
default:
|
|
1895
|
+
return null;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
if (matched === 0) {
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
const normalized = text.replace(/\s+/g, "");
|
|
1902
|
+
const consumed = Array.from(normalized.matchAll(DURATION_PART_RE)).map((match) => match[0]).join("");
|
|
1903
|
+
if (consumed !== normalized) {
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
return total;
|
|
1907
|
+
}
|
|
1908
|
+
function parseDurationInput(value, nowMs = Date.now()) {
|
|
1909
|
+
if (!value || !value.trim()) {
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1912
|
+
const durationMs = durationToMs(value.trim());
|
|
1913
|
+
if (durationMs == null) {
|
|
1914
|
+
return void 0;
|
|
1915
|
+
}
|
|
1916
|
+
return nowMs - durationMs;
|
|
1917
|
+
}
|
|
1918
|
+
function parseTimeBoundaryInput(value, _nowMs = Date.now()) {
|
|
1919
|
+
if (!value || !value.trim()) {
|
|
1920
|
+
return void 0;
|
|
1921
|
+
}
|
|
1922
|
+
const trimmed = value.trim();
|
|
1923
|
+
const numeric = Number(trimmed);
|
|
1924
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
1925
|
+
return numeric > 1e10 ? Math.trunc(numeric) : Math.trunc(numeric * 1e3);
|
|
1926
|
+
}
|
|
1927
|
+
const parsed = Date.parse(trimmed);
|
|
1928
|
+
if (Number.isFinite(parsed)) {
|
|
1929
|
+
return parsed;
|
|
1930
|
+
}
|
|
1931
|
+
return void 0;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
635
1934
|
// src/lib/text-send.ts
|
|
636
1935
|
import { ThreadType } from "zca-js";
|
|
637
1936
|
|
|
@@ -1016,9 +2315,9 @@ async function resolveGroupMentionsIfNeeded(params, text) {
|
|
|
1016
2315
|
|
|
1017
2316
|
// src/lib/video-send.ts
|
|
1018
2317
|
import { execFile } from "child_process";
|
|
1019
|
-
import
|
|
2318
|
+
import fs5 from "fs/promises";
|
|
1020
2319
|
import os3 from "os";
|
|
1021
|
-
import
|
|
2320
|
+
import path5 from "path";
|
|
1022
2321
|
import { promisify } from "util";
|
|
1023
2322
|
var execFileAsync = promisify(execFile);
|
|
1024
2323
|
function planVideoSendMode(params) {
|
|
@@ -1035,7 +2334,7 @@ function planVideoSendMode(params) {
|
|
|
1035
2334
|
reason: "native-video mode supports one video at a time"
|
|
1036
2335
|
};
|
|
1037
2336
|
}
|
|
1038
|
-
const ext =
|
|
2337
|
+
const ext = path5.extname(files[0] ?? "").toLowerCase();
|
|
1039
2338
|
if (ext !== ".mp4") {
|
|
1040
2339
|
return {
|
|
1041
2340
|
mode: "attachment",
|
|
@@ -1129,8 +2428,8 @@ async function maybeProbeVideoFile(filePath) {
|
|
|
1129
2428
|
}
|
|
1130
2429
|
}
|
|
1131
2430
|
async function generateVideoThumbnail(videoPath) {
|
|
1132
|
-
const dir = await
|
|
1133
|
-
const outputPath =
|
|
2431
|
+
const dir = await fs5.mkdtemp(path5.join(os3.tmpdir(), "openzca-video-thumb-"));
|
|
2432
|
+
const outputPath = path5.join(dir, "thumbnail.jpg");
|
|
1134
2433
|
try {
|
|
1135
2434
|
await runBinary("ffmpeg", [
|
|
1136
2435
|
"-y",
|
|
@@ -1144,15 +2443,15 @@ async function generateVideoThumbnail(videoPath) {
|
|
|
1144
2443
|
"2",
|
|
1145
2444
|
outputPath
|
|
1146
2445
|
]);
|
|
1147
|
-
await
|
|
2446
|
+
await fs5.access(outputPath);
|
|
1148
2447
|
} catch (error) {
|
|
1149
|
-
await
|
|
2448
|
+
await fs5.rm(dir, { recursive: true, force: true });
|
|
1150
2449
|
throw error;
|
|
1151
2450
|
}
|
|
1152
2451
|
return {
|
|
1153
2452
|
path: outputPath,
|
|
1154
2453
|
cleanup: async () => {
|
|
1155
|
-
await
|
|
2454
|
+
await fs5.rm(dir, { recursive: true, force: true });
|
|
1156
2455
|
}
|
|
1157
2456
|
};
|
|
1158
2457
|
}
|
|
@@ -1195,33 +2494,258 @@ async function sendNativeVideo(params) {
|
|
|
1195
2494
|
}
|
|
1196
2495
|
}
|
|
1197
2496
|
|
|
1198
|
-
// src/
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
2497
|
+
// src/lib/reply.ts
|
|
2498
|
+
import { ThreadType as ThreadType2 } from "zca-js";
|
|
2499
|
+
function prepareReplyMessage(value, params) {
|
|
2500
|
+
const sourceRecord = asReplyMessageRecord(value);
|
|
2501
|
+
const metadata = asOptionalReplyMessageRecord(sourceRecord.metadata);
|
|
2502
|
+
const rawMessageRecord = asOptionalReplyMessageRecord(sourceRecord.rawMessage);
|
|
2503
|
+
const rawPayloadRecord = asOptionalReplyMessageRecord(sourceRecord.rawPayload);
|
|
2504
|
+
const canonicalRecord = rawMessageRecord ?? sourceRecord;
|
|
2505
|
+
const content = parseReplyMessageContent(
|
|
2506
|
+
canonicalRecord.content ?? sourceRecord.content,
|
|
2507
|
+
isLikelyOpenzcaListenPayload(sourceRecord) && !rawMessageRecord
|
|
2508
|
+
);
|
|
2509
|
+
const msgType = requireStringLike(
|
|
2510
|
+
[canonicalRecord.msgType, sourceRecord.msgType, metadata?.msgType],
|
|
2511
|
+
"reply message msgType"
|
|
2512
|
+
);
|
|
2513
|
+
const uidFrom = requireStringLike(
|
|
2514
|
+
[
|
|
2515
|
+
canonicalRecord.uidFrom,
|
|
2516
|
+
sourceRecord.uidFrom,
|
|
2517
|
+
sourceRecord.senderId,
|
|
2518
|
+
sourceRecord.fromId,
|
|
2519
|
+
metadata?.senderId,
|
|
2520
|
+
metadata?.fromId
|
|
2521
|
+
],
|
|
2522
|
+
"reply message uidFrom"
|
|
2523
|
+
);
|
|
2524
|
+
const msgId = requireStringLike(
|
|
2525
|
+
[canonicalRecord.msgId, sourceRecord.msgId, rawPayloadRecord?.msgId],
|
|
2526
|
+
"reply message msgId"
|
|
2527
|
+
);
|
|
2528
|
+
const cliMsgId = requireStringLike(
|
|
2529
|
+
[canonicalRecord.cliMsgId, sourceRecord.cliMsgId, rawPayloadRecord?.cliMsgId],
|
|
2530
|
+
"reply message cliMsgId"
|
|
2531
|
+
);
|
|
2532
|
+
const ts = requireTsString(
|
|
2533
|
+
[canonicalRecord.ts, sourceRecord.ts, maybeTimestampSecondsToMsString(sourceRecord.timestamp)],
|
|
2534
|
+
"reply message ts"
|
|
2535
|
+
);
|
|
2536
|
+
const ttl = parseReplyMessageTtl(canonicalRecord.ttl ?? sourceRecord.ttl);
|
|
2537
|
+
const propertyExt = parseReplyMessagePropertyExt(canonicalRecord.propertyExt);
|
|
2538
|
+
return {
|
|
2539
|
+
quote: {
|
|
2540
|
+
content,
|
|
2541
|
+
msgType,
|
|
2542
|
+
propertyExt,
|
|
2543
|
+
uidFrom,
|
|
2544
|
+
msgId,
|
|
2545
|
+
cliMsgId,
|
|
2546
|
+
ts,
|
|
2547
|
+
ttl
|
|
2548
|
+
},
|
|
2549
|
+
inferredThreadId: inferReplyMessageThreadId({
|
|
2550
|
+
sourceRecord,
|
|
2551
|
+
canonicalRecord,
|
|
2552
|
+
metadata,
|
|
2553
|
+
threadType: params?.threadType,
|
|
2554
|
+
selfId: params?.selfId
|
|
2555
|
+
})
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
function prepareStoredReplyMessage(value, params) {
|
|
2559
|
+
const record = asReplyMessageRecord(value);
|
|
2560
|
+
const storedThreadType = record.threadType === "group" ? ThreadType2.Group : record.threadType === "user" ? ThreadType2.User : void 0;
|
|
2561
|
+
if (storedThreadType !== void 0 && storedThreadType !== params.threadType) {
|
|
2562
|
+
throw new Error("Reply source thread type does not match --group.");
|
|
2563
|
+
}
|
|
2564
|
+
const storedThreadId = firstString([record.threadId, record.rawThreadId]) ?? void 0;
|
|
2565
|
+
if (storedThreadId && storedThreadId !== params.threadId) {
|
|
2566
|
+
throw new Error("Reply source belongs to a different thread.");
|
|
2567
|
+
}
|
|
2568
|
+
const rawMessage = asOptionalReplyMessageRecord(record.rawMessage);
|
|
2569
|
+
const rawPayload = asOptionalReplyMessageRecord(record.rawPayload);
|
|
2570
|
+
const replyRecord = rawMessage ?? rawPayload;
|
|
2571
|
+
if (!replyRecord) {
|
|
2572
|
+
throw new Error(
|
|
2573
|
+
"Reply source found in DB but has no reusable raw message payload. Re-sync or capture it via listener first."
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
return prepareReplyMessage(replyRecord, {
|
|
2577
|
+
threadType: params.threadType,
|
|
2578
|
+
selfId: params.selfId
|
|
2579
|
+
}).quote;
|
|
2580
|
+
}
|
|
2581
|
+
function asReplyMessageRecord(value) {
|
|
2582
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2583
|
+
throw new Error("Reply message must be a JSON object matching the raw message.data shape.");
|
|
2584
|
+
}
|
|
2585
|
+
return value;
|
|
2586
|
+
}
|
|
2587
|
+
function asOptionalReplyMessageRecord(value) {
|
|
2588
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2589
|
+
return void 0;
|
|
2590
|
+
}
|
|
2591
|
+
return value;
|
|
2592
|
+
}
|
|
2593
|
+
function parseReplyMessageContent(value, stripOpenzcaDecorations) {
|
|
2594
|
+
if (typeof value === "string") {
|
|
2595
|
+
return stripOpenzcaDecorations ? stripEnrichedReplyDecorations(value) : value;
|
|
2596
|
+
}
|
|
2597
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2598
|
+
return value;
|
|
2599
|
+
}
|
|
2600
|
+
throw new Error("Reply message content must be a string or object.");
|
|
2601
|
+
}
|
|
2602
|
+
function stripEnrichedReplyDecorations(value) {
|
|
2603
|
+
const lines = value.split("\n");
|
|
2604
|
+
while (lines.length > 0) {
|
|
2605
|
+
const last = lines[lines.length - 1].trim();
|
|
2606
|
+
if (last.startsWith("[reply context: ") || last.startsWith("[reply media attached:") || last.startsWith("[reply media attached ")) {
|
|
2607
|
+
lines.pop();
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
break;
|
|
2611
|
+
}
|
|
2612
|
+
return lines.join("\n");
|
|
2613
|
+
}
|
|
2614
|
+
function parseReplyMessagePropertyExt(value) {
|
|
2615
|
+
if (value === void 0) {
|
|
2616
|
+
return void 0;
|
|
2617
|
+
}
|
|
2618
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2619
|
+
throw new Error("Reply message propertyExt must be an object when provided.");
|
|
2620
|
+
}
|
|
2621
|
+
return value;
|
|
2622
|
+
}
|
|
2623
|
+
function parseReplyMessageTtl(value) {
|
|
2624
|
+
if (value === void 0 || value === null || value === "") {
|
|
2625
|
+
return 0;
|
|
2626
|
+
}
|
|
2627
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
2628
|
+
if (!Number.isFinite(parsed)) {
|
|
2629
|
+
throw new Error("Reply message ttl must be a finite number.");
|
|
2630
|
+
}
|
|
2631
|
+
return Math.trunc(parsed);
|
|
2632
|
+
}
|
|
2633
|
+
function requireStringLike(values, label) {
|
|
2634
|
+
const value = firstString(values);
|
|
2635
|
+
if (!value) {
|
|
2636
|
+
throw new Error(`Missing ${label}.`);
|
|
2637
|
+
}
|
|
2638
|
+
return value;
|
|
2639
|
+
}
|
|
2640
|
+
function requireTsString(values, label) {
|
|
2641
|
+
for (const value of values) {
|
|
2642
|
+
if (typeof value === "string" && value.trim()) {
|
|
2643
|
+
return value.trim();
|
|
2644
|
+
}
|
|
2645
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2646
|
+
return String(Math.trunc(value));
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
throw new Error(`Missing ${label}.`);
|
|
2650
|
+
}
|
|
2651
|
+
function firstString(values) {
|
|
2652
|
+
for (const value of values) {
|
|
2653
|
+
if (typeof value === "string" && value.trim()) {
|
|
2654
|
+
return value.trim();
|
|
2655
|
+
}
|
|
2656
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2657
|
+
return String(Math.trunc(value));
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
return void 0;
|
|
2661
|
+
}
|
|
2662
|
+
function maybeTimestampSecondsToMsString(value) {
|
|
2663
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2664
|
+
return void 0;
|
|
2665
|
+
}
|
|
2666
|
+
return String(Math.trunc(value * 1e3));
|
|
2667
|
+
}
|
|
2668
|
+
function isLikelyOpenzcaListenPayload(record) {
|
|
2669
|
+
return typeof record.threadId === "string" && (typeof record.senderId === "string" || typeof record.chatType === "string" || typeof record.metadata === "object");
|
|
2670
|
+
}
|
|
2671
|
+
function inferReplyMessageThreadId(params) {
|
|
2672
|
+
const directThreadId = firstString([
|
|
2673
|
+
params.sourceRecord.threadId,
|
|
2674
|
+
params.sourceRecord.targetId,
|
|
2675
|
+
params.sourceRecord.conversationId,
|
|
2676
|
+
params.metadata?.threadId,
|
|
2677
|
+
params.metadata?.targetId
|
|
2678
|
+
]);
|
|
2679
|
+
if (directThreadId) {
|
|
2680
|
+
return directThreadId;
|
|
2681
|
+
}
|
|
2682
|
+
if (params.threadType === void 0) {
|
|
2683
|
+
return void 0;
|
|
2684
|
+
}
|
|
2685
|
+
const idTo = firstString([
|
|
2686
|
+
params.canonicalRecord.idTo,
|
|
2687
|
+
params.sourceRecord.idTo,
|
|
2688
|
+
params.sourceRecord.toId,
|
|
2689
|
+
params.metadata?.toId
|
|
2690
|
+
]);
|
|
2691
|
+
if (params.threadType === ThreadType2.Group) {
|
|
2692
|
+
return idTo;
|
|
2693
|
+
}
|
|
2694
|
+
const uidFrom = firstString([
|
|
2695
|
+
params.canonicalRecord.uidFrom,
|
|
2696
|
+
params.sourceRecord.uidFrom,
|
|
2697
|
+
params.sourceRecord.senderId,
|
|
2698
|
+
params.sourceRecord.fromId,
|
|
2699
|
+
params.metadata?.senderId,
|
|
2700
|
+
params.metadata?.fromId
|
|
2701
|
+
]);
|
|
2702
|
+
if (!uidFrom && !idTo) {
|
|
2703
|
+
return void 0;
|
|
2704
|
+
}
|
|
2705
|
+
if (params.selfId) {
|
|
2706
|
+
if (uidFrom && uidFrom !== params.selfId && uidFrom !== "0") {
|
|
2707
|
+
return uidFrom;
|
|
2708
|
+
}
|
|
2709
|
+
if (idTo && idTo !== params.selfId && idTo !== "0") {
|
|
2710
|
+
return idTo;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
if (uidFrom && uidFrom !== "0") {
|
|
2714
|
+
return uidFrom;
|
|
2715
|
+
}
|
|
2716
|
+
if (idTo && idTo !== "0") {
|
|
2717
|
+
return idTo;
|
|
2718
|
+
}
|
|
2719
|
+
return void 0;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// src/cli.ts
|
|
2723
|
+
var require2 = createRequire(import.meta.url);
|
|
2724
|
+
var { version: PKG_VERSION } = require2("../package.json");
|
|
2725
|
+
var program = new Command();
|
|
2726
|
+
var EMOJI_REACTION_MAP = {
|
|
2727
|
+
"\u2764\uFE0F": Reactions.HEART,
|
|
2728
|
+
"\u2764": Reactions.HEART,
|
|
2729
|
+
"\u{1F44D}": Reactions.LIKE,
|
|
2730
|
+
"\u{1F606}": Reactions.HAHA,
|
|
2731
|
+
"\u{1F602}": Reactions.HAHA,
|
|
2732
|
+
"\u{1F62E}": Reactions.WOW,
|
|
2733
|
+
"\u{1F62D}": Reactions.CRY,
|
|
2734
|
+
"\u{1F621}": Reactions.ANGRY
|
|
2735
|
+
};
|
|
2736
|
+
var DEBUG_COMMAND_START = /* @__PURE__ */ new WeakMap();
|
|
2737
|
+
function parseDebugFlag(value) {
|
|
2738
|
+
if (!value) return false;
|
|
2739
|
+
const normalized = value.trim().toLowerCase();
|
|
2740
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
2741
|
+
}
|
|
2742
|
+
function getActionCommand(args) {
|
|
2743
|
+
for (let index = args.length - 1; index >= 0; index -= 1) {
|
|
2744
|
+
const item = args[index];
|
|
2745
|
+
if (item instanceof Command) {
|
|
2746
|
+
return item;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
1225
2749
|
return void 0;
|
|
1226
2750
|
}
|
|
1227
2751
|
function commandPathLabel(command) {
|
|
@@ -1237,6 +2761,25 @@ function commandPathLabel(command) {
|
|
|
1237
2761
|
}
|
|
1238
2762
|
return names.join(" ");
|
|
1239
2763
|
}
|
|
2764
|
+
function readCliFlag(names) {
|
|
2765
|
+
const argv = process.argv.slice(2);
|
|
2766
|
+
return argv.some((item) => names.includes(item));
|
|
2767
|
+
}
|
|
2768
|
+
function readCliOptionValue(names) {
|
|
2769
|
+
const argv = process.argv.slice(2);
|
|
2770
|
+
for (let index = argv.length - 1; index >= 0; index -= 1) {
|
|
2771
|
+
const item = argv[index];
|
|
2772
|
+
for (const name of names) {
|
|
2773
|
+
if (item === name) {
|
|
2774
|
+
return argv[index + 1];
|
|
2775
|
+
}
|
|
2776
|
+
if (item.startsWith(`${name}=`)) {
|
|
2777
|
+
return item.slice(name.length + 1);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return void 0;
|
|
2782
|
+
}
|
|
1240
2783
|
function getDebugOptions(command) {
|
|
1241
2784
|
if (command) {
|
|
1242
2785
|
if (typeof command.optsWithGlobals === "function") {
|
|
@@ -1257,9 +2800,9 @@ function resolveDebugEnabled(command) {
|
|
|
1257
2800
|
}
|
|
1258
2801
|
function resolveDebugFilePath(command) {
|
|
1259
2802
|
const options = getDebugOptions(command);
|
|
1260
|
-
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() ||
|
|
2803
|
+
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() || path6.join(APP_HOME, "logs", "openzca-debug.log");
|
|
1261
2804
|
const normalized = normalizeMediaInput(configured);
|
|
1262
|
-
return
|
|
2805
|
+
return path6.isAbsolute(normalized) ? normalized : path6.resolve(process.cwd(), normalized);
|
|
1263
2806
|
}
|
|
1264
2807
|
function writeDebugLine(event, details, command) {
|
|
1265
2808
|
if (!resolveDebugEnabled(command)) {
|
|
@@ -1270,7 +2813,7 @@ function writeDebugLine(event, details, command) {
|
|
|
1270
2813
|
`;
|
|
1271
2814
|
const filePath = resolveDebugFilePath(command);
|
|
1272
2815
|
try {
|
|
1273
|
-
fsSync.mkdirSync(
|
|
2816
|
+
fsSync.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
1274
2817
|
fsSync.appendFileSync(filePath, line, "utf8");
|
|
1275
2818
|
} catch {
|
|
1276
2819
|
}
|
|
@@ -1318,8 +2861,27 @@ function output(value, asJson = false) {
|
|
|
1318
2861
|
}
|
|
1319
2862
|
console.log(String(value));
|
|
1320
2863
|
}
|
|
2864
|
+
function shouldOutputJson(opts) {
|
|
2865
|
+
return Boolean(opts?.json) || readCliFlag(["--json", "-j"]);
|
|
2866
|
+
}
|
|
2867
|
+
function normalizeCommandAliases(argv) {
|
|
2868
|
+
const normalized = [...argv];
|
|
2869
|
+
const dbIndex = normalized.indexOf("db");
|
|
2870
|
+
if (dbIndex === -1 || normalized[dbIndex + 1] !== "chat") {
|
|
2871
|
+
return normalized;
|
|
2872
|
+
}
|
|
2873
|
+
const subcommandOrId = normalized[dbIndex + 2];
|
|
2874
|
+
if (!subcommandOrId || subcommandOrId.startsWith("-")) {
|
|
2875
|
+
return normalized;
|
|
2876
|
+
}
|
|
2877
|
+
if (["list", "info", "messages", "help"].includes(subcommandOrId)) {
|
|
2878
|
+
return normalized;
|
|
2879
|
+
}
|
|
2880
|
+
normalized.splice(dbIndex + 2, 0, "messages");
|
|
2881
|
+
return normalized;
|
|
2882
|
+
}
|
|
1321
2883
|
function asThreadType(groupFlag) {
|
|
1322
|
-
return groupFlag ?
|
|
2884
|
+
return groupFlag ? ThreadType3.Group : ThreadType3.User;
|
|
1323
2885
|
}
|
|
1324
2886
|
function parseBooleanFromEnv(name, fallback) {
|
|
1325
2887
|
const raw = process.env[name]?.trim();
|
|
@@ -1358,14 +2920,14 @@ function collectIdsFromCacheEntries(entries, keys) {
|
|
|
1358
2920
|
return ids;
|
|
1359
2921
|
}
|
|
1360
2922
|
function getListenerOwnerLockPath(profile) {
|
|
1361
|
-
return
|
|
2923
|
+
return path6.join(getProfileDir(profile), "listener-owner.json");
|
|
1362
2924
|
}
|
|
1363
2925
|
function getListenIpcSocketPath(profile) {
|
|
1364
2926
|
if (process.platform === "win32") {
|
|
1365
2927
|
const safe = profile.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
1366
2928
|
return `\\\\.\\pipe\\openzca-listen-${safe}`;
|
|
1367
2929
|
}
|
|
1368
|
-
return
|
|
2930
|
+
return path6.join(getProfileDir(profile), "listen.sock");
|
|
1369
2931
|
}
|
|
1370
2932
|
function parsePositiveIntFromUnknown(value) {
|
|
1371
2933
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
@@ -1392,7 +2954,7 @@ function isProcessAlive(pid) {
|
|
|
1392
2954
|
}
|
|
1393
2955
|
async function readListenerOwnerRecord(lockPath) {
|
|
1394
2956
|
try {
|
|
1395
|
-
const raw = await
|
|
2957
|
+
const raw = await fs6.readFile(lockPath, "utf8");
|
|
1396
2958
|
const parsed = JSON.parse(raw);
|
|
1397
2959
|
const pid = parsePositiveIntFromUnknown(parsed.pid);
|
|
1398
2960
|
if (!pid) return null;
|
|
@@ -1412,11 +2974,11 @@ async function readActiveListenerOwner(profile) {
|
|
|
1412
2974
|
const lockPath = getListenerOwnerLockPath(profile);
|
|
1413
2975
|
const record = await readListenerOwnerRecord(lockPath);
|
|
1414
2976
|
if (!record) {
|
|
1415
|
-
await
|
|
2977
|
+
await fs6.rm(lockPath, { force: true });
|
|
1416
2978
|
return null;
|
|
1417
2979
|
}
|
|
1418
2980
|
if (!isProcessAlive(record.pid)) {
|
|
1419
|
-
await
|
|
2981
|
+
await fs6.rm(lockPath, { force: true });
|
|
1420
2982
|
return null;
|
|
1421
2983
|
}
|
|
1422
2984
|
return record;
|
|
@@ -1432,7 +2994,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
1432
2994
|
};
|
|
1433
2995
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1434
2996
|
try {
|
|
1435
|
-
await
|
|
2997
|
+
await fs6.writeFile(lockPath, `${JSON.stringify(record, null, 2)}
|
|
1436
2998
|
`, {
|
|
1437
2999
|
encoding: "utf8",
|
|
1438
3000
|
flag: "wx"
|
|
@@ -1445,7 +3007,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
1445
3007
|
released = true;
|
|
1446
3008
|
const current = await readListenerOwnerRecord(lockPath);
|
|
1447
3009
|
if (current && current.pid !== process.pid) return;
|
|
1448
|
-
await
|
|
3010
|
+
await fs6.rm(lockPath, { force: true });
|
|
1449
3011
|
writeDebugLine(
|
|
1450
3012
|
"listen.owner.released",
|
|
1451
3013
|
{
|
|
@@ -1466,7 +3028,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
1466
3028
|
`Another openzca listener already owns profile "${profile}" (pid ${owner.pid}).`
|
|
1467
3029
|
);
|
|
1468
3030
|
}
|
|
1469
|
-
await
|
|
3031
|
+
await fs6.rm(lockPath, { force: true });
|
|
1470
3032
|
}
|
|
1471
3033
|
}
|
|
1472
3034
|
throw new Error(`Unable to acquire listener ownership for profile "${profile}".`);
|
|
@@ -1484,7 +3046,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
1484
3046
|
}
|
|
1485
3047
|
const socketPath = getListenIpcSocketPath(profile);
|
|
1486
3048
|
if (process.platform !== "win32") {
|
|
1487
|
-
await
|
|
3049
|
+
await fs6.rm(socketPath, { force: true });
|
|
1488
3050
|
}
|
|
1489
3051
|
const uploadTimeoutMs = parsePositiveIntFromEnv(
|
|
1490
3052
|
"OPENZCA_UPLOAD_IPC_HANDLER_TIMEOUT_MS",
|
|
@@ -1529,7 +3091,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
1529
3091
|
fail(parsed.requestId, "Invalid upload payload.");
|
|
1530
3092
|
return;
|
|
1531
3093
|
}
|
|
1532
|
-
const threadType = parsed.threadType === "group" ?
|
|
3094
|
+
const threadType = parsed.threadType === "group" ? ThreadType3.Group : ThreadType3.User;
|
|
1533
3095
|
const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
|
|
1534
3096
|
writeDebugLine(
|
|
1535
3097
|
"listen.ipc.upload.start",
|
|
@@ -1656,7 +3218,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
1656
3218
|
server.close(() => resolve());
|
|
1657
3219
|
});
|
|
1658
3220
|
if (process.platform !== "win32") {
|
|
1659
|
-
await
|
|
3221
|
+
await fs6.rm(socketPath, { force: true });
|
|
1660
3222
|
}
|
|
1661
3223
|
writeDebugLine(
|
|
1662
3224
|
"listen.ipc.stopped",
|
|
@@ -1686,7 +3248,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1686
3248
|
{
|
|
1687
3249
|
profile,
|
|
1688
3250
|
threadId,
|
|
1689
|
-
threadType: threadType ===
|
|
3251
|
+
threadType: threadType === ThreadType3.Group ? "group" : "user",
|
|
1690
3252
|
attachmentCount: attachments.length,
|
|
1691
3253
|
socketPath,
|
|
1692
3254
|
requestId,
|
|
@@ -1730,7 +3292,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1730
3292
|
requestId,
|
|
1731
3293
|
profile,
|
|
1732
3294
|
threadId,
|
|
1733
|
-
threadType: threadType ===
|
|
3295
|
+
threadType: threadType === ThreadType3.Group ? "group" : "user",
|
|
1734
3296
|
attachments
|
|
1735
3297
|
};
|
|
1736
3298
|
socket.write(`${JSON.stringify(payload)}
|
|
@@ -1792,21 +3354,21 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1792
3354
|
}
|
|
1793
3355
|
async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
|
|
1794
3356
|
if (groupFlag) {
|
|
1795
|
-
return { type:
|
|
3357
|
+
return { type: ThreadType3.Group, reason: "explicit_group_flag" };
|
|
1796
3358
|
}
|
|
1797
3359
|
const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", false);
|
|
1798
3360
|
if (!autoDetectEnabled) {
|
|
1799
|
-
return { type:
|
|
3361
|
+
return { type: ThreadType3.User, reason: "auto_detect_disabled" };
|
|
1800
3362
|
}
|
|
1801
3363
|
try {
|
|
1802
3364
|
const cache = await readCache(profile);
|
|
1803
3365
|
const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
|
|
1804
3366
|
if (groupIds.has(threadId)) {
|
|
1805
|
-
return { type:
|
|
3367
|
+
return { type: ThreadType3.Group, reason: "cache_group_match" };
|
|
1806
3368
|
}
|
|
1807
3369
|
const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
|
|
1808
3370
|
if (friendIds.has(threadId)) {
|
|
1809
|
-
return { type:
|
|
3371
|
+
return { type: ThreadType3.User, reason: "cache_friend_match" };
|
|
1810
3372
|
}
|
|
1811
3373
|
} catch (error) {
|
|
1812
3374
|
writeDebugLine(
|
|
@@ -1821,7 +3383,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1821
3383
|
}
|
|
1822
3384
|
const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
|
|
1823
3385
|
if (!probeEnabled) {
|
|
1824
|
-
return { type:
|
|
3386
|
+
return { type: ThreadType3.User, reason: "probe_disabled" };
|
|
1825
3387
|
}
|
|
1826
3388
|
const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
|
|
1827
3389
|
try {
|
|
@@ -1831,7 +3393,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1831
3393
|
`Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
|
|
1832
3394
|
);
|
|
1833
3395
|
if (groupInfo?.gridInfoMap?.[threadId]) {
|
|
1834
|
-
return { type:
|
|
3396
|
+
return { type: ThreadType3.Group, reason: "probe_group_match" };
|
|
1835
3397
|
}
|
|
1836
3398
|
} catch (error) {
|
|
1837
3399
|
writeDebugLine(
|
|
@@ -1844,7 +3406,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1844
3406
|
command
|
|
1845
3407
|
);
|
|
1846
3408
|
}
|
|
1847
|
-
return { type:
|
|
3409
|
+
return { type: ThreadType3.User, reason: "default_user" };
|
|
1848
3410
|
}
|
|
1849
3411
|
function parseReaction(input) {
|
|
1850
3412
|
const normalized = input.trim();
|
|
@@ -1927,6 +3489,675 @@ async function requireApi(command) {
|
|
|
1927
3489
|
const api = await loginWithStoredCredentials(profile);
|
|
1928
3490
|
return { profile, api };
|
|
1929
3491
|
}
|
|
3492
|
+
function toDbThreadType(groupFlag) {
|
|
3493
|
+
return groupFlag ? "group" : "user";
|
|
3494
|
+
}
|
|
3495
|
+
function getDbWriteOverride(opts) {
|
|
3496
|
+
if (!opts || typeof opts.db !== "boolean") {
|
|
3497
|
+
return void 0;
|
|
3498
|
+
}
|
|
3499
|
+
return opts.db;
|
|
3500
|
+
}
|
|
3501
|
+
async function shouldWriteToDb(profile, override) {
|
|
3502
|
+
if (typeof override === "boolean") {
|
|
3503
|
+
return override;
|
|
3504
|
+
}
|
|
3505
|
+
return isDbEnabled(profile);
|
|
3506
|
+
}
|
|
3507
|
+
async function resolveSendReplyQuote(params) {
|
|
3508
|
+
const replyId = params.replyId?.trim();
|
|
3509
|
+
const replyMessage = params.replyMessage?.trim();
|
|
3510
|
+
if (replyId && replyMessage) {
|
|
3511
|
+
throw new Error("Use either --reply-id or --reply-message, not both.");
|
|
3512
|
+
}
|
|
3513
|
+
if (!replyId && !replyMessage) {
|
|
3514
|
+
return void 0;
|
|
3515
|
+
}
|
|
3516
|
+
if (replyId) {
|
|
3517
|
+
if (!await shouldWriteToDb(params.profile)) {
|
|
3518
|
+
throw new Error("`--reply-id` requires the local DB. Enable DB/listen sync first.");
|
|
3519
|
+
}
|
|
3520
|
+
const row = await getMessageById({
|
|
3521
|
+
profile: params.profile,
|
|
3522
|
+
id: replyId
|
|
3523
|
+
});
|
|
3524
|
+
if (!row) {
|
|
3525
|
+
throw new Error(`Reply source not found in DB: ${replyId}`);
|
|
3526
|
+
}
|
|
3527
|
+
if (row.threadType === "group" !== (params.threadType === ThreadType3.Group)) {
|
|
3528
|
+
throw new Error("Reply source thread type does not match --group.");
|
|
3529
|
+
}
|
|
3530
|
+
if (row.threadId !== params.threadId) {
|
|
3531
|
+
throw new Error("Reply source belongs to a different thread.");
|
|
3532
|
+
}
|
|
3533
|
+
if (!row.rawMessage || typeof row.rawMessage !== "object") {
|
|
3534
|
+
if (!row.rawPayload || typeof row.rawPayload !== "object") {
|
|
3535
|
+
throw new Error(
|
|
3536
|
+
"Reply source found in DB but has no reusable raw message payload. Re-sync or capture it via listener first."
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
return prepareStoredReplyMessage(row, {
|
|
3541
|
+
threadId: params.threadId,
|
|
3542
|
+
threadType: params.threadType,
|
|
3543
|
+
selfId: params.api.getOwnId()
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
let parsedReplyMessage;
|
|
3547
|
+
try {
|
|
3548
|
+
parsedReplyMessage = JSON.parse(replyMessage);
|
|
3549
|
+
} catch (error) {
|
|
3550
|
+
throw new Error(
|
|
3551
|
+
`Invalid JSON for --reply-message: ${error instanceof Error ? error.message : String(error)}`
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
const preparedReply = prepareReplyMessage(parsedReplyMessage, {
|
|
3555
|
+
threadType: params.threadType,
|
|
3556
|
+
selfId: params.api.getOwnId()
|
|
3557
|
+
});
|
|
3558
|
+
if (preparedReply.inferredThreadId && preparedReply.inferredThreadId !== params.threadId) {
|
|
3559
|
+
throw new Error("Reply message belongs to a different thread.");
|
|
3560
|
+
}
|
|
3561
|
+
return preparedReply.quote;
|
|
3562
|
+
}
|
|
3563
|
+
function scheduleDbWrite(profile, command, event, task) {
|
|
3564
|
+
enqueueDbWrite(profile, async () => {
|
|
3565
|
+
try {
|
|
3566
|
+
await task();
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
writeDebugLine(
|
|
3569
|
+
event,
|
|
3570
|
+
{
|
|
3571
|
+
profile,
|
|
3572
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3573
|
+
},
|
|
3574
|
+
command
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
function extractResponseMessageIds(value) {
|
|
3580
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3581
|
+
const visit = (item) => {
|
|
3582
|
+
if (!item) return;
|
|
3583
|
+
if (Array.isArray(item)) {
|
|
3584
|
+
for (const nested of item) {
|
|
3585
|
+
visit(nested);
|
|
3586
|
+
}
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
if (typeof item !== "object") {
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
const record = item;
|
|
3593
|
+
const msgId = normalizeCachedId(record.msgId);
|
|
3594
|
+
if (msgId) {
|
|
3595
|
+
ids.add(msgId);
|
|
3596
|
+
}
|
|
3597
|
+
for (const key of ["message", "attachment", "attachments", "results", "response"]) {
|
|
3598
|
+
if (key in record) {
|
|
3599
|
+
visit(record[key]);
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
visit(value);
|
|
3604
|
+
return Array.from(ids);
|
|
3605
|
+
}
|
|
3606
|
+
async function persistOutgoingMessageBestEffort(params) {
|
|
3607
|
+
const selfId = params.api.getOwnId();
|
|
3608
|
+
const threadType = toDbThreadType(params.group);
|
|
3609
|
+
const scopeThreadId = resolveScopeThreadId({
|
|
3610
|
+
threadType,
|
|
3611
|
+
rawThreadId: params.threadId,
|
|
3612
|
+
senderId: selfId,
|
|
3613
|
+
toId: params.threadId,
|
|
3614
|
+
selfId
|
|
3615
|
+
});
|
|
3616
|
+
const messageIds = extractResponseMessageIds(params.response);
|
|
3617
|
+
const baseRecord = {
|
|
3618
|
+
profile: params.profile,
|
|
3619
|
+
scopeThreadId,
|
|
3620
|
+
rawThreadId: params.threadId,
|
|
3621
|
+
threadType,
|
|
3622
|
+
peerId: threadType === "user" ? scopeThreadId : void 0,
|
|
3623
|
+
senderId: selfId,
|
|
3624
|
+
senderName: void 0,
|
|
3625
|
+
toId: threadType === "user" ? params.threadId : void 0,
|
|
3626
|
+
timestampMs: Date.now(),
|
|
3627
|
+
msgType: params.msgType,
|
|
3628
|
+
contentText: params.text,
|
|
3629
|
+
media: params.media,
|
|
3630
|
+
source: "send",
|
|
3631
|
+
rawPayloadJson: params.rawPayload ? JSON.stringify(params.rawPayload) : void 0,
|
|
3632
|
+
rawMessageJson: JSON.stringify(params.response)
|
|
3633
|
+
};
|
|
3634
|
+
if (messageIds.length === 0) {
|
|
3635
|
+
await persistMessage(baseRecord);
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
for (const msgId of messageIds) {
|
|
3639
|
+
await persistMessage({
|
|
3640
|
+
...baseRecord,
|
|
3641
|
+
msgId
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
async function persistGroupMembersSnapshot(profile, groupId, api) {
|
|
3646
|
+
const rows = await listGroupMemberRows(api, groupId);
|
|
3647
|
+
const snapshotAtMs = Date.now();
|
|
3648
|
+
await replaceThreadMembers(
|
|
3649
|
+
profile,
|
|
3650
|
+
groupId,
|
|
3651
|
+
rows.map((row) => ({
|
|
3652
|
+
profile,
|
|
3653
|
+
scopeThreadId: groupId,
|
|
3654
|
+
userId: row.userId,
|
|
3655
|
+
displayName: row.displayName,
|
|
3656
|
+
zaloName: row.zaloName,
|
|
3657
|
+
rawJson: JSON.stringify(row),
|
|
3658
|
+
snapshotAtMs
|
|
3659
|
+
}))
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
async function persistFriendDirectory(profile, api) {
|
|
3663
|
+
const friends = await api.getAllFriends();
|
|
3664
|
+
const nameById = /* @__PURE__ */ new Map();
|
|
3665
|
+
for (const friend2 of friends) {
|
|
3666
|
+
const record = friend2;
|
|
3667
|
+
const userId = normalizeCachedId(record.userId);
|
|
3668
|
+
if (!userId) continue;
|
|
3669
|
+
const displayName = typeof record.displayName === "string" && record.displayName.trim() ? record.displayName.trim() : void 0;
|
|
3670
|
+
const zaloName = typeof record.zaloName === "string" && record.zaloName.trim() ? record.zaloName.trim() : void 0;
|
|
3671
|
+
const avatar = typeof record.avatar === "string" && record.avatar.trim() ? record.avatar.trim() : void 0;
|
|
3672
|
+
const title = displayName || zaloName || userId;
|
|
3673
|
+
await persistFriend({
|
|
3674
|
+
profile,
|
|
3675
|
+
userId,
|
|
3676
|
+
displayName,
|
|
3677
|
+
zaloName,
|
|
3678
|
+
avatar,
|
|
3679
|
+
accountStatus: typeof record.accountStatus === "number" && Number.isFinite(record.accountStatus) ? Math.trunc(record.accountStatus) : void 0,
|
|
3680
|
+
rawJson: JSON.stringify(friend2)
|
|
3681
|
+
});
|
|
3682
|
+
await persistThread({
|
|
3683
|
+
profile,
|
|
3684
|
+
scopeThreadId: userId,
|
|
3685
|
+
rawThreadId: userId,
|
|
3686
|
+
threadType: "user",
|
|
3687
|
+
peerId: userId,
|
|
3688
|
+
title,
|
|
3689
|
+
rawJson: JSON.stringify(friend2)
|
|
3690
|
+
});
|
|
3691
|
+
nameById.set(userId, title);
|
|
3692
|
+
}
|
|
3693
|
+
return nameById;
|
|
3694
|
+
}
|
|
3695
|
+
function parseSinceDuration(label, value) {
|
|
3696
|
+
const parsed = parseDurationInput(value);
|
|
3697
|
+
if (parsed !== void 0) {
|
|
3698
|
+
return parsed;
|
|
3699
|
+
}
|
|
3700
|
+
if (!value || !value.trim()) {
|
|
3701
|
+
return void 0;
|
|
3702
|
+
}
|
|
3703
|
+
throw new Error(
|
|
3704
|
+
`${label} must be a relative duration like 30s, 7m, 24h, 7d, or 2w.`
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
function parseTimeBoundary(label, value) {
|
|
3708
|
+
const parsed = parseTimeBoundaryInput(value);
|
|
3709
|
+
if (parsed !== void 0) {
|
|
3710
|
+
return parsed;
|
|
3711
|
+
}
|
|
3712
|
+
if (!value || !value.trim()) {
|
|
3713
|
+
return void 0;
|
|
3714
|
+
}
|
|
3715
|
+
throw new Error(
|
|
3716
|
+
`${label} must be an ISO timestamp, a date, or unix seconds/ms.`
|
|
3717
|
+
);
|
|
3718
|
+
}
|
|
3719
|
+
function pickExclusiveOption(primaryLabel, primaryValue, aliasLabel, aliasValue) {
|
|
3720
|
+
if (primaryValue?.trim() && aliasValue?.trim()) {
|
|
3721
|
+
throw new Error(`Use either ${primaryLabel} or ${aliasLabel}, not both.`);
|
|
3722
|
+
}
|
|
3723
|
+
return primaryValue?.trim() ? primaryValue : aliasValue?.trim() ? aliasValue : void 0;
|
|
3724
|
+
}
|
|
3725
|
+
function resolveMessageTimeRange(opts) {
|
|
3726
|
+
const sinceValue = opts.since?.trim() ? opts.since : void 0;
|
|
3727
|
+
const fromValue = opts.from?.trim() ? opts.from : void 0;
|
|
3728
|
+
const untilValue = pickExclusiveOption("--until", opts.until, "--to", opts.to);
|
|
3729
|
+
if (sinceValue && fromValue) {
|
|
3730
|
+
throw new Error("Use either --since for a rolling window or --from/--to for an explicit range, not both.");
|
|
3731
|
+
}
|
|
3732
|
+
if (sinceValue && untilValue) {
|
|
3733
|
+
throw new Error("Do not combine --since with --to/--until. Use --from/--to for explicit ranges.");
|
|
3734
|
+
}
|
|
3735
|
+
return {
|
|
3736
|
+
sinceMs: sinceValue ? parseSinceDuration("--since", sinceValue) : parseTimeBoundary("--from", fromValue),
|
|
3737
|
+
untilMs: parseTimeBoundary("--to/--until", untilValue)
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
function resolveMessageQueryOptions(opts) {
|
|
3741
|
+
const { sinceMs, untilMs } = resolveMessageTimeRange(opts);
|
|
3742
|
+
if (opts.all && opts.limit?.trim()) {
|
|
3743
|
+
throw new Error("Use either --all or --limit, not both.");
|
|
3744
|
+
}
|
|
3745
|
+
const explicitLimit = parsePositiveIntOption("--limit", opts.limit);
|
|
3746
|
+
const hasTimeFilter = sinceMs !== void 0 || untilMs !== void 0;
|
|
3747
|
+
const limit = opts.all ? void 0 : explicitLimit ?? (hasTimeFilter ? void 0 : 20);
|
|
3748
|
+
const newestFirst = !Boolean(opts.oldestFirst);
|
|
3749
|
+
return {
|
|
3750
|
+
sinceMs,
|
|
3751
|
+
untilMs,
|
|
3752
|
+
limit,
|
|
3753
|
+
newestFirst
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
async function resolveStoredChatThreadType(profile, chatId, forceGroup) {
|
|
3757
|
+
if (forceGroup) {
|
|
3758
|
+
return "group";
|
|
3759
|
+
}
|
|
3760
|
+
const row = await getThreadInfo({ profile, threadId: chatId });
|
|
3761
|
+
return row?.threadType === "group" ? "group" : "user";
|
|
3762
|
+
}
|
|
3763
|
+
async function confirmDestructiveAction(message) {
|
|
3764
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3765
|
+
throw new Error("Refusing destructive operation without --yes in non-interactive mode.");
|
|
3766
|
+
}
|
|
3767
|
+
const rl = readline.createInterface({
|
|
3768
|
+
input: process.stdin,
|
|
3769
|
+
output: process.stdout
|
|
3770
|
+
});
|
|
3771
|
+
try {
|
|
3772
|
+
const answer = (await rl.question(`${message} [y/N] `)).trim().toLowerCase();
|
|
3773
|
+
return answer === "y" || answer === "yes";
|
|
3774
|
+
} finally {
|
|
3775
|
+
rl.close();
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
function createSyncProgressReporter() {
|
|
3779
|
+
if (!process.stderr.isTTY) {
|
|
3780
|
+
return () => {
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
return (message) => {
|
|
3784
|
+
process.stderr.write(`[db sync] ${message}
|
|
3785
|
+
`);
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
function createDbSyncSummary(profile, dbPath, count) {
|
|
3789
|
+
return {
|
|
3790
|
+
profile,
|
|
3791
|
+
dbPath,
|
|
3792
|
+
windowCount: count,
|
|
3793
|
+
groupsSynced: 0,
|
|
3794
|
+
groupMessagesImported: 0,
|
|
3795
|
+
friendsSynced: 0,
|
|
3796
|
+
chatsSynced: 0,
|
|
3797
|
+
dmMessagesImported: 0,
|
|
3798
|
+
syncState: []
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
function resolveSyncWindowCount(value) {
|
|
3802
|
+
return parsePositiveIntOption("--count", value) ?? 200;
|
|
3803
|
+
}
|
|
3804
|
+
async function collectConversationIds(api) {
|
|
3805
|
+
let pinnedIds = /* @__PURE__ */ new Set();
|
|
3806
|
+
let hiddenIds = /* @__PURE__ */ new Set();
|
|
3807
|
+
try {
|
|
3808
|
+
const pins = await api.getPinConversations();
|
|
3809
|
+
pinnedIds = new Set((pins.conversations ?? []).map((value) => String(value)));
|
|
3810
|
+
} catch {
|
|
3811
|
+
}
|
|
3812
|
+
try {
|
|
3813
|
+
const hidden = await api.getHiddenConversations();
|
|
3814
|
+
hiddenIds = new Set((hidden.threads ?? []).map((item) => String(item.thread_id)));
|
|
3815
|
+
} catch {
|
|
3816
|
+
}
|
|
3817
|
+
return { pinnedIds, hiddenIds };
|
|
3818
|
+
}
|
|
3819
|
+
async function prepareDbGroupTarget(params) {
|
|
3820
|
+
await persistThread({
|
|
3821
|
+
profile: params.profile,
|
|
3822
|
+
scopeThreadId: params.groupId,
|
|
3823
|
+
rawThreadId: params.groupId,
|
|
3824
|
+
threadType: "group",
|
|
3825
|
+
title: params.title,
|
|
3826
|
+
isPinned: params.pinnedIds.has(params.groupId),
|
|
3827
|
+
isHidden: params.hiddenIds.has(params.groupId),
|
|
3828
|
+
rawJson: params.rawJson
|
|
3829
|
+
});
|
|
3830
|
+
await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
|
|
3831
|
+
}
|
|
3832
|
+
async function syncDbGroupHistoryFull(params) {
|
|
3833
|
+
if (params.targetGroupIds.size === 0) {
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
const getStoredGroupMessageCount = async () => {
|
|
3837
|
+
let total = 0;
|
|
3838
|
+
for (const groupId of params.targetGroupIds) {
|
|
3839
|
+
const row = await getThreadInfo({
|
|
3840
|
+
profile: params.profile,
|
|
3841
|
+
threadId: groupId,
|
|
3842
|
+
threadType: "group"
|
|
3843
|
+
});
|
|
3844
|
+
const count = row && typeof row.messageCount === "number" && Number.isFinite(row.messageCount) ? row.messageCount : 0;
|
|
3845
|
+
total += count;
|
|
3846
|
+
}
|
|
3847
|
+
return total;
|
|
3848
|
+
};
|
|
3849
|
+
const persistMessages = async (messages) => {
|
|
3850
|
+
for (const message of messages) {
|
|
3851
|
+
if (!params.targetGroupIds.has(message.threadId)) {
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
processed += 1;
|
|
3855
|
+
await persistMessage(
|
|
3856
|
+
toDbRecordFromRecentMessage({
|
|
3857
|
+
profile: params.profile,
|
|
3858
|
+
message,
|
|
3859
|
+
source: "sync_group",
|
|
3860
|
+
selfId: params.selfId,
|
|
3861
|
+
title: params.titleById.get(message.threadId)
|
|
3862
|
+
})
|
|
3863
|
+
);
|
|
3864
|
+
}
|
|
3865
|
+
};
|
|
3866
|
+
const beforeCount = await getStoredGroupMessageCount();
|
|
3867
|
+
let processed = 0;
|
|
3868
|
+
let completeness = "complete";
|
|
3869
|
+
let stopReason = "exhausted";
|
|
3870
|
+
let pagesRequested = 0;
|
|
3871
|
+
let listenerImportedCount = 0;
|
|
3872
|
+
try {
|
|
3873
|
+
params.progress?.(`syncing full history for ${params.targetGroupIds.size} group(s)`);
|
|
3874
|
+
const result = await crawlGroupHistoryViaListener(params.api, {
|
|
3875
|
+
maxPages: Number.MAX_SAFE_INTEGER,
|
|
3876
|
+
idleTimeoutMs: 15e3,
|
|
3877
|
+
onMessages: persistMessages,
|
|
3878
|
+
onPage: ({ pagesRequested: pagesRequested2, filteredCount }) => {
|
|
3879
|
+
params.progress?.(
|
|
3880
|
+
`groups page ${pagesRequested2}: batch ${filteredCount}, processed ${processed}`
|
|
3881
|
+
);
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
completeness = result.stopReason === "exhausted" ? "complete" : result.stopReason === "max_pages" || result.stopReason === "timeout" ? "partial" : "window";
|
|
3885
|
+
stopReason = result.stopReason;
|
|
3886
|
+
pagesRequested = result.pagesRequested;
|
|
3887
|
+
listenerImportedCount = await getStoredGroupMessageCount() - beforeCount;
|
|
3888
|
+
} catch (error) {
|
|
3889
|
+
stopReason = `fallback_window:${toErrorText(error)}`;
|
|
3890
|
+
completeness = "window";
|
|
3891
|
+
}
|
|
3892
|
+
const fallbackCount = 200;
|
|
3893
|
+
params.progress?.(`merging recent group API window (${fallbackCount} per group)`);
|
|
3894
|
+
const beforeApiCount = await getStoredGroupMessageCount();
|
|
3895
|
+
for (const groupId of params.targetGroupIds) {
|
|
3896
|
+
const messages = await fetchRecentGroupMessagesViaApi(params.api, groupId, fallbackCount);
|
|
3897
|
+
await persistMessages(messages);
|
|
3898
|
+
params.progress?.(`group ${groupId}: fetched ${messages.length} message(s) from group history API`);
|
|
3899
|
+
}
|
|
3900
|
+
const afterCount = await getStoredGroupMessageCount();
|
|
3901
|
+
const apiAddedCount = afterCount - beforeApiCount;
|
|
3902
|
+
if (apiAddedCount > 0) {
|
|
3903
|
+
completeness = "window";
|
|
3904
|
+
if (stopReason === "exhausted" && listenerImportedCount === 0) {
|
|
3905
|
+
stopReason = "fallback_window:empty_listener_result";
|
|
3906
|
+
} else if (stopReason === "exhausted") {
|
|
3907
|
+
stopReason = "window_topoff:listener_incomplete";
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
const imported = Math.max(afterCount - beforeCount, 0);
|
|
3911
|
+
for (const groupId of params.targetGroupIds) {
|
|
3912
|
+
await setSyncState({
|
|
3913
|
+
profile: params.profile,
|
|
3914
|
+
scopeThreadId: groupId,
|
|
3915
|
+
threadType: "group",
|
|
3916
|
+
status: "synced",
|
|
3917
|
+
completeness
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
params.summary.groupsSynced += params.targetGroupIds.size;
|
|
3921
|
+
params.summary.groupMessagesImported += imported;
|
|
3922
|
+
params.summary.syncState.push({
|
|
3923
|
+
kind: "groups",
|
|
3924
|
+
groups: params.targetGroupIds.size,
|
|
3925
|
+
imported,
|
|
3926
|
+
completeness,
|
|
3927
|
+
stopReason,
|
|
3928
|
+
pagesRequested
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
async function syncDbFriendDirectory(params) {
|
|
3932
|
+
params.progress?.("syncing friend directory");
|
|
3933
|
+
const names = await persistFriendDirectory(params.profile, params.api);
|
|
3934
|
+
params.summary.friendsSynced += names.size;
|
|
3935
|
+
params.progress?.(`friend directory synced: ${names.size} friend(s)`);
|
|
3936
|
+
params.summary.syncState.push({
|
|
3937
|
+
kind: "friends",
|
|
3938
|
+
imported: names.size
|
|
3939
|
+
});
|
|
3940
|
+
return names;
|
|
3941
|
+
}
|
|
3942
|
+
async function syncDbChatThread(params) {
|
|
3943
|
+
const scopeThreadId = resolveScopeThreadId({
|
|
3944
|
+
threadType: "user",
|
|
3945
|
+
rawThreadId: params.threadId,
|
|
3946
|
+
senderId: params.selfId,
|
|
3947
|
+
toId: params.threadId,
|
|
3948
|
+
selfId: params.selfId
|
|
3949
|
+
});
|
|
3950
|
+
await persistThread({
|
|
3951
|
+
profile: params.profile,
|
|
3952
|
+
scopeThreadId,
|
|
3953
|
+
rawThreadId: params.threadId,
|
|
3954
|
+
threadType: "user",
|
|
3955
|
+
peerId: scopeThreadId,
|
|
3956
|
+
title: params.title,
|
|
3957
|
+
isPinned: params.pinnedIds.has(params.threadId) || params.pinnedIds.has(scopeThreadId),
|
|
3958
|
+
isHidden: params.hiddenIds.has(params.threadId) || params.hiddenIds.has(scopeThreadId)
|
|
3959
|
+
});
|
|
3960
|
+
const messages = await fetchRecentUserMessagesViaListener(params.api, params.threadId, params.count);
|
|
3961
|
+
for (const message of messages) {
|
|
3962
|
+
await persistMessage(
|
|
3963
|
+
toDbRecordFromRecentMessage({
|
|
3964
|
+
profile: params.profile,
|
|
3965
|
+
message,
|
|
3966
|
+
source: "sync_dm_best_effort",
|
|
3967
|
+
selfId: params.selfId,
|
|
3968
|
+
title: params.title
|
|
3969
|
+
})
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
await setSyncState({
|
|
3973
|
+
profile: params.profile,
|
|
3974
|
+
scopeThreadId,
|
|
3975
|
+
threadType: "user",
|
|
3976
|
+
status: "synced",
|
|
3977
|
+
completeness: "best_effort"
|
|
3978
|
+
});
|
|
3979
|
+
params.summary.chatsSynced += 1;
|
|
3980
|
+
params.summary.dmMessagesImported += messages.length;
|
|
3981
|
+
params.progress?.(`chat ${scopeThreadId}: imported ${messages.length} message(s)`);
|
|
3982
|
+
params.summary.syncState.push({
|
|
3983
|
+
kind: "chat",
|
|
3984
|
+
chatId: scopeThreadId,
|
|
3985
|
+
rawThreadId: params.threadId,
|
|
3986
|
+
imported: messages.length,
|
|
3987
|
+
completeness: "best_effort"
|
|
3988
|
+
});
|
|
3989
|
+
}
|
|
3990
|
+
async function syncDbChatsBestEffort(params) {
|
|
3991
|
+
const scanLimit = Math.max(params.count * 10, 500);
|
|
3992
|
+
params.progress?.(`scanning recent DM/chat windows (target window ${params.count}, scan limit ${scanLimit})`);
|
|
3993
|
+
const messages = await fetchRecentUserMessagesAcrossThreads(params.api, scanLimit);
|
|
3994
|
+
const seenScopes = /* @__PURE__ */ new Set();
|
|
3995
|
+
for (const message of messages) {
|
|
3996
|
+
const title = params.titleById.get(message.threadId);
|
|
3997
|
+
const record = toDbRecordFromRecentMessage({
|
|
3998
|
+
profile: params.profile,
|
|
3999
|
+
message,
|
|
4000
|
+
source: "sync_dm_best_effort",
|
|
4001
|
+
selfId: params.selfId,
|
|
4002
|
+
title
|
|
4003
|
+
});
|
|
4004
|
+
await persistThread({
|
|
4005
|
+
profile: params.profile,
|
|
4006
|
+
scopeThreadId: record.scopeThreadId,
|
|
4007
|
+
rawThreadId: record.rawThreadId,
|
|
4008
|
+
threadType: "user",
|
|
4009
|
+
peerId: record.scopeThreadId,
|
|
4010
|
+
title,
|
|
4011
|
+
isPinned: params.pinnedIds.has(record.rawThreadId) || params.pinnedIds.has(record.scopeThreadId),
|
|
4012
|
+
isHidden: params.hiddenIds.has(record.rawThreadId) || params.hiddenIds.has(record.scopeThreadId)
|
|
4013
|
+
});
|
|
4014
|
+
await persistMessage(record);
|
|
4015
|
+
if (!seenScopes.has(record.scopeThreadId)) {
|
|
4016
|
+
seenScopes.add(record.scopeThreadId);
|
|
4017
|
+
await setSyncState({
|
|
4018
|
+
profile: params.profile,
|
|
4019
|
+
scopeThreadId: record.scopeThreadId,
|
|
4020
|
+
threadType: "user",
|
|
4021
|
+
status: "synced",
|
|
4022
|
+
completeness: "best_effort"
|
|
4023
|
+
});
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
params.summary.chatsSynced += seenScopes.size;
|
|
4027
|
+
params.summary.dmMessagesImported += messages.length;
|
|
4028
|
+
params.progress?.(`chat scan finished: ${messages.length} message(s) across ${seenScopes.size} chat(s)`);
|
|
4029
|
+
params.summary.syncState.push({
|
|
4030
|
+
kind: "chats",
|
|
4031
|
+
imported: messages.length,
|
|
4032
|
+
chats: seenScopes.size,
|
|
4033
|
+
completeness: "best_effort"
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
async function runDbSync(params) {
|
|
4037
|
+
const { profile, api } = await requireApi(params.command);
|
|
4038
|
+
const dbPath = await resolveDbPath(profile);
|
|
4039
|
+
params.progress?.(`starting sync for profile ${profile}`);
|
|
4040
|
+
const summary = createDbSyncSummary(
|
|
4041
|
+
profile,
|
|
4042
|
+
dbPath,
|
|
4043
|
+
params.mode === "all" || params.mode === "chats" || params.mode === "chat" ? params.count : void 0
|
|
4044
|
+
);
|
|
4045
|
+
const selfId = api.getOwnId();
|
|
4046
|
+
const selfInfo = normalizeMeInfoOutput(await api.fetchAccountInfo());
|
|
4047
|
+
await persistSelfProfile({
|
|
4048
|
+
profile,
|
|
4049
|
+
userId: selfId,
|
|
4050
|
+
displayName: typeof selfInfo.displayName === "string" && selfInfo.displayName.trim() ? selfInfo.displayName.trim() : void 0,
|
|
4051
|
+
infoJson: JSON.stringify(selfInfo)
|
|
4052
|
+
});
|
|
4053
|
+
const { pinnedIds, hiddenIds } = await collectConversationIds(api);
|
|
4054
|
+
let friendNames = /* @__PURE__ */ new Map();
|
|
4055
|
+
if (params.mode === "all" || params.mode === "friends" || params.mode === "chats") {
|
|
4056
|
+
friendNames = await syncDbFriendDirectory({
|
|
4057
|
+
profile,
|
|
4058
|
+
api,
|
|
4059
|
+
summary,
|
|
4060
|
+
progress: params.progress
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
if (params.mode === "all" || params.mode === "groups") {
|
|
4064
|
+
const groups = await buildGroupsDetailed(api);
|
|
4065
|
+
const targetGroupIds = /* @__PURE__ */ new Set();
|
|
4066
|
+
const titleById = /* @__PURE__ */ new Map();
|
|
4067
|
+
for (const group2 of groups) {
|
|
4068
|
+
const record = group2;
|
|
4069
|
+
const groupId = normalizeCachedId(record.groupId);
|
|
4070
|
+
if (!groupId) continue;
|
|
4071
|
+
const title = typeof record.name === "string" && record.name.trim() ? record.name.trim() : typeof record.groupName === "string" && record.groupName.trim() ? record.groupName.trim() : void 0;
|
|
4072
|
+
targetGroupIds.add(groupId);
|
|
4073
|
+
titleById.set(groupId, title);
|
|
4074
|
+
await prepareDbGroupTarget({
|
|
4075
|
+
profile,
|
|
4076
|
+
api,
|
|
4077
|
+
groupId,
|
|
4078
|
+
title,
|
|
4079
|
+
rawJson: JSON.stringify(group2),
|
|
4080
|
+
pinnedIds,
|
|
4081
|
+
hiddenIds
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
4084
|
+
await syncDbGroupHistoryFull({
|
|
4085
|
+
profile,
|
|
4086
|
+
api,
|
|
4087
|
+
selfId,
|
|
4088
|
+
targetGroupIds,
|
|
4089
|
+
titleById,
|
|
4090
|
+
summary,
|
|
4091
|
+
progress: params.progress
|
|
4092
|
+
});
|
|
4093
|
+
}
|
|
4094
|
+
if (params.mode === "group") {
|
|
4095
|
+
if (!params.groupId) {
|
|
4096
|
+
throw new Error("Missing group id for db sync group.");
|
|
4097
|
+
}
|
|
4098
|
+
const groupInfo = await api.getGroupInfo(params.groupId);
|
|
4099
|
+
const group2 = groupInfo.gridInfoMap[params.groupId];
|
|
4100
|
+
const title = typeof group2?.name === "string" && group2.name.trim() ? group2.name.trim() : void 0;
|
|
4101
|
+
await prepareDbGroupTarget({
|
|
4102
|
+
profile,
|
|
4103
|
+
api,
|
|
4104
|
+
groupId: params.groupId,
|
|
4105
|
+
title,
|
|
4106
|
+
rawJson: group2 ? JSON.stringify(group2) : void 0,
|
|
4107
|
+
pinnedIds,
|
|
4108
|
+
hiddenIds
|
|
4109
|
+
});
|
|
4110
|
+
await syncDbGroupHistoryFull({
|
|
4111
|
+
profile,
|
|
4112
|
+
api,
|
|
4113
|
+
selfId,
|
|
4114
|
+
targetGroupIds: /* @__PURE__ */ new Set([params.groupId]),
|
|
4115
|
+
titleById: /* @__PURE__ */ new Map([[params.groupId, title]]),
|
|
4116
|
+
summary,
|
|
4117
|
+
progress: params.progress
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
if (params.mode === "chat") {
|
|
4121
|
+
if (!params.threadId) {
|
|
4122
|
+
throw new Error("Missing chat id for db sync chat.");
|
|
4123
|
+
}
|
|
4124
|
+
if (friendNames.size === 0) {
|
|
4125
|
+
friendNames = await persistFriendDirectory(profile, api);
|
|
4126
|
+
}
|
|
4127
|
+
await syncDbChatThread({
|
|
4128
|
+
profile,
|
|
4129
|
+
api,
|
|
4130
|
+
selfId,
|
|
4131
|
+
threadId: params.threadId,
|
|
4132
|
+
count: params.count,
|
|
4133
|
+
title: friendNames.get(params.threadId),
|
|
4134
|
+
pinnedIds,
|
|
4135
|
+
hiddenIds,
|
|
4136
|
+
summary,
|
|
4137
|
+
progress: params.progress
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
if (params.mode === "all" || params.mode === "chats") {
|
|
4141
|
+
if (friendNames.size === 0) {
|
|
4142
|
+
friendNames = await persistFriendDirectory(profile, api);
|
|
4143
|
+
}
|
|
4144
|
+
await syncDbChatsBestEffort({
|
|
4145
|
+
profile,
|
|
4146
|
+
api,
|
|
4147
|
+
selfId,
|
|
4148
|
+
count: params.count,
|
|
4149
|
+
titleById: friendNames,
|
|
4150
|
+
pinnedIds,
|
|
4151
|
+
hiddenIds,
|
|
4152
|
+
summary,
|
|
4153
|
+
progress: params.progress
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
params.progress?.(
|
|
4157
|
+
`done: groups=${summary.groupsSynced}, groupMessages=${summary.groupMessagesImported}, friends=${summary.friendsSynced}, chats=${summary.chatsSynced}, dmMessages=${summary.dmMessagesImported}`
|
|
4158
|
+
);
|
|
4159
|
+
return summary;
|
|
4160
|
+
}
|
|
1930
4161
|
async function buildGroupsDetailed(api) {
|
|
1931
4162
|
const groups = await api.getAllGroups();
|
|
1932
4163
|
const ids = Object.keys(groups.gridVerMap ?? {});
|
|
@@ -2309,7 +4540,7 @@ function normalizeGroupHistoryMessages(messages, fallbackThreadId) {
|
|
|
2309
4540
|
const threadIdRaw = String(raw.idTo ?? "").trim();
|
|
2310
4541
|
normalized.push({
|
|
2311
4542
|
threadId: threadIdRaw || fallbackThreadId,
|
|
2312
|
-
type:
|
|
4543
|
+
type: ThreadType3.Group,
|
|
2313
4544
|
data: {
|
|
2314
4545
|
actionId: typeof raw.actionId === "string" && raw.actionId.trim() ? raw.actionId : void 0,
|
|
2315
4546
|
msgId: String(raw.msgId ?? ""),
|
|
@@ -2383,12 +4614,168 @@ async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
|
|
|
2383
4614
|
return fetchRecentGroupMessagesViaListener(api, threadId, count);
|
|
2384
4615
|
}
|
|
2385
4616
|
async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
4617
|
+
const result = await crawlGroupHistoryViaListener(api, {
|
|
4618
|
+
threadId,
|
|
4619
|
+
limit: count,
|
|
4620
|
+
maxPages: parsePositiveIntFromEnv("OPENZCA_RECENT_GROUP_MAX_PAGES", 20),
|
|
4621
|
+
idleTimeoutMs: 12e3
|
|
4622
|
+
});
|
|
4623
|
+
return result.messages;
|
|
4624
|
+
}
|
|
4625
|
+
async function crawlGroupHistoryViaListener(api, options) {
|
|
4626
|
+
return new Promise((resolve, reject) => {
|
|
4627
|
+
let settled = false;
|
|
4628
|
+
let stopReason = "closed";
|
|
4629
|
+
const shouldCollect = options.limit != null || !options.onMessages;
|
|
4630
|
+
const collected = [];
|
|
4631
|
+
const seenMessageKeys = /* @__PURE__ */ new Set();
|
|
4632
|
+
const requestedCursors = /* @__PURE__ */ new Set();
|
|
4633
|
+
let pagesRequested = 0;
|
|
4634
|
+
let idleTimer;
|
|
4635
|
+
let processing = Promise.resolve();
|
|
4636
|
+
const toKey = (message) => {
|
|
4637
|
+
const msgId = String(message.data?.msgId ?? "");
|
|
4638
|
+
const cliMsgId = String(message.data?.cliMsgId ?? "");
|
|
4639
|
+
return `${message.threadId}:${msgId}:${cliMsgId}`;
|
|
4640
|
+
};
|
|
4641
|
+
const requestPage = (lastId) => {
|
|
4642
|
+
const cursor = String(lastId ?? "").trim();
|
|
4643
|
+
if (cursor) {
|
|
4644
|
+
if (requestedCursors.has(cursor)) return false;
|
|
4645
|
+
requestedCursors.add(cursor);
|
|
4646
|
+
}
|
|
4647
|
+
pagesRequested += 1;
|
|
4648
|
+
api.listener.requestOldMessages(ThreadType3.Group, cursor || null);
|
|
4649
|
+
return true;
|
|
4650
|
+
};
|
|
4651
|
+
const armIdleTimer = () => {
|
|
4652
|
+
if (idleTimer) {
|
|
4653
|
+
clearTimeout(idleTimer);
|
|
4654
|
+
}
|
|
4655
|
+
idleTimer = setTimeout(() => {
|
|
4656
|
+
finish(void 0, "timeout");
|
|
4657
|
+
}, options.idleTimeoutMs);
|
|
4658
|
+
};
|
|
4659
|
+
const cleanup = () => {
|
|
4660
|
+
if (idleTimer) {
|
|
4661
|
+
clearTimeout(idleTimer);
|
|
4662
|
+
}
|
|
4663
|
+
api.listener.off("connected", onConnected);
|
|
4664
|
+
api.listener.off("old_messages", onOldMessages);
|
|
4665
|
+
api.listener.off("error", onError);
|
|
4666
|
+
api.listener.off("closed", onClosed);
|
|
4667
|
+
try {
|
|
4668
|
+
api.listener.stop();
|
|
4669
|
+
} catch {
|
|
4670
|
+
}
|
|
4671
|
+
};
|
|
4672
|
+
const finish = (error, reason) => {
|
|
4673
|
+
if (settled) return;
|
|
4674
|
+
settled = true;
|
|
4675
|
+
if (reason) {
|
|
4676
|
+
stopReason = reason;
|
|
4677
|
+
}
|
|
4678
|
+
void processing.then(() => {
|
|
4679
|
+
cleanup();
|
|
4680
|
+
if (error) {
|
|
4681
|
+
reject(error);
|
|
4682
|
+
return;
|
|
4683
|
+
}
|
|
4684
|
+
resolve({
|
|
4685
|
+
messages: options.limit != null ? sortRecentMessagesNewestFirst(collected).slice(0, options.limit) : collected,
|
|
4686
|
+
stopReason,
|
|
4687
|
+
pagesRequested
|
|
4688
|
+
});
|
|
4689
|
+
}).catch((processingError) => {
|
|
4690
|
+
cleanup();
|
|
4691
|
+
reject(processingError);
|
|
4692
|
+
});
|
|
4693
|
+
};
|
|
4694
|
+
const onConnected = () => {
|
|
4695
|
+
try {
|
|
4696
|
+
armIdleTimer();
|
|
4697
|
+
requestPage(null);
|
|
4698
|
+
} catch (error) {
|
|
4699
|
+
finish(error, "closed");
|
|
4700
|
+
}
|
|
4701
|
+
};
|
|
4702
|
+
const onOldMessages = (messages, type) => {
|
|
4703
|
+
if (type !== ThreadType3.Group) return;
|
|
4704
|
+
armIdleTimer();
|
|
4705
|
+
const typedMessages = messages;
|
|
4706
|
+
processing = processing.then(async () => {
|
|
4707
|
+
const filtered = [];
|
|
4708
|
+
for (const message of typedMessages) {
|
|
4709
|
+
if (options.threadId && message.threadId !== options.threadId) {
|
|
4710
|
+
continue;
|
|
4711
|
+
}
|
|
4712
|
+
const key = toKey(message);
|
|
4713
|
+
if (seenMessageKeys.has(key)) continue;
|
|
4714
|
+
seenMessageKeys.add(key);
|
|
4715
|
+
if (shouldCollect) {
|
|
4716
|
+
collected.push(message);
|
|
4717
|
+
}
|
|
4718
|
+
filtered.push(message);
|
|
4719
|
+
}
|
|
4720
|
+
if (filtered.length > 0) {
|
|
4721
|
+
await options.onMessages?.(filtered);
|
|
4722
|
+
}
|
|
4723
|
+
await options.onPage?.({
|
|
4724
|
+
pagesRequested,
|
|
4725
|
+
filteredCount: filtered.length,
|
|
4726
|
+
collectedCount: collected.length
|
|
4727
|
+
});
|
|
4728
|
+
if (options.limit != null && collected.length >= options.limit) {
|
|
4729
|
+
finish(void 0, "limit");
|
|
4730
|
+
return;
|
|
4731
|
+
}
|
|
4732
|
+
if (typedMessages.length === 0) {
|
|
4733
|
+
finish(void 0, "exhausted");
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
if (pagesRequested >= options.maxPages) {
|
|
4737
|
+
finish(void 0, "max_pages");
|
|
4738
|
+
return;
|
|
4739
|
+
}
|
|
4740
|
+
const cursorCandidates = getRecentPageCursors(typedMessages);
|
|
4741
|
+
let requested = false;
|
|
4742
|
+
for (const cursor of cursorCandidates) {
|
|
4743
|
+
if (requestPage(cursor)) {
|
|
4744
|
+
requested = true;
|
|
4745
|
+
break;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
if (!requested) {
|
|
4749
|
+
finish(void 0, "exhausted");
|
|
4750
|
+
}
|
|
4751
|
+
}).catch((error) => {
|
|
4752
|
+
finish(error, "closed");
|
|
4753
|
+
});
|
|
4754
|
+
};
|
|
4755
|
+
const onError = (error) => {
|
|
4756
|
+
finish(error, "closed");
|
|
4757
|
+
};
|
|
4758
|
+
const onClosed = () => {
|
|
4759
|
+
finish(void 0, "closed");
|
|
4760
|
+
};
|
|
4761
|
+
api.listener.on("connected", onConnected);
|
|
4762
|
+
api.listener.on("old_messages", onOldMessages);
|
|
4763
|
+
api.listener.on("error", onError);
|
|
4764
|
+
api.listener.on("closed", onClosed);
|
|
4765
|
+
try {
|
|
4766
|
+
api.listener.start();
|
|
4767
|
+
} catch (error) {
|
|
4768
|
+
finish(error);
|
|
4769
|
+
}
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
2386
4773
|
return new Promise((resolve, reject) => {
|
|
2387
4774
|
let settled = false;
|
|
2388
4775
|
const collected = [];
|
|
2389
4776
|
const seenMessageKeys = /* @__PURE__ */ new Set();
|
|
2390
4777
|
const requestedCursors = /* @__PURE__ */ new Set();
|
|
2391
|
-
const maxPages = parsePositiveIntFromEnv("
|
|
4778
|
+
const maxPages = parsePositiveIntFromEnv("OPENZCA_RECENT_USER_MAX_PAGES", 20);
|
|
2392
4779
|
let pagesRequested = 0;
|
|
2393
4780
|
const toKey = (message) => {
|
|
2394
4781
|
const msgId = String(message.data?.msgId ?? "");
|
|
@@ -2402,7 +4789,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
2402
4789
|
requestedCursors.add(cursor);
|
|
2403
4790
|
}
|
|
2404
4791
|
pagesRequested += 1;
|
|
2405
|
-
api.listener.requestOldMessages(
|
|
4792
|
+
api.listener.requestOldMessages(ThreadType3.User, cursor || null);
|
|
2406
4793
|
return true;
|
|
2407
4794
|
};
|
|
2408
4795
|
const cleanup = () => {
|
|
@@ -2434,7 +4821,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
2434
4821
|
}
|
|
2435
4822
|
};
|
|
2436
4823
|
const onOldMessages = (messages, type) => {
|
|
2437
|
-
if (type !==
|
|
4824
|
+
if (type !== ThreadType3.User) return;
|
|
2438
4825
|
const typedMessages = messages;
|
|
2439
4826
|
for (const message of typedMessages) {
|
|
2440
4827
|
if (message.threadId === threadId) {
|
|
@@ -2490,7 +4877,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
2490
4877
|
}
|
|
2491
4878
|
});
|
|
2492
4879
|
}
|
|
2493
|
-
async function
|
|
4880
|
+
async function fetchRecentUserMessagesAcrossThreads(api, maxMessages) {
|
|
2494
4881
|
return new Promise((resolve, reject) => {
|
|
2495
4882
|
let settled = false;
|
|
2496
4883
|
const collected = [];
|
|
@@ -2501,7 +4888,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2501
4888
|
const toKey = (message) => {
|
|
2502
4889
|
const msgId = String(message.data?.msgId ?? "");
|
|
2503
4890
|
const cliMsgId = String(message.data?.cliMsgId ?? "");
|
|
2504
|
-
return `${msgId}:${cliMsgId}`;
|
|
4891
|
+
return `${message.threadId}:${msgId}:${cliMsgId}`;
|
|
2505
4892
|
};
|
|
2506
4893
|
const requestPage = (lastId) => {
|
|
2507
4894
|
const cursor = String(lastId ?? "").trim();
|
|
@@ -2510,7 +4897,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2510
4897
|
requestedCursors.add(cursor);
|
|
2511
4898
|
}
|
|
2512
4899
|
pagesRequested += 1;
|
|
2513
|
-
api.listener.requestOldMessages(
|
|
4900
|
+
api.listener.requestOldMessages(ThreadType3.User, cursor || null);
|
|
2514
4901
|
return true;
|
|
2515
4902
|
};
|
|
2516
4903
|
const cleanup = () => {
|
|
@@ -2532,7 +4919,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2532
4919
|
reject(error);
|
|
2533
4920
|
return;
|
|
2534
4921
|
}
|
|
2535
|
-
resolve(sortRecentMessagesNewestFirst(collected).slice(0,
|
|
4922
|
+
resolve(sortRecentMessagesNewestFirst(collected).slice(0, maxMessages));
|
|
2536
4923
|
};
|
|
2537
4924
|
const onConnected = () => {
|
|
2538
4925
|
try {
|
|
@@ -2542,25 +4929,15 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2542
4929
|
}
|
|
2543
4930
|
};
|
|
2544
4931
|
const onOldMessages = (messages, type) => {
|
|
2545
|
-
if (type !==
|
|
4932
|
+
if (type !== ThreadType3.User) return;
|
|
2546
4933
|
const typedMessages = messages;
|
|
2547
4934
|
for (const message of typedMessages) {
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
collected.push(message);
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
if (collected.length >= count) {
|
|
2556
|
-
finish();
|
|
2557
|
-
return;
|
|
2558
|
-
}
|
|
2559
|
-
if (typedMessages.length === 0) {
|
|
2560
|
-
finish();
|
|
2561
|
-
return;
|
|
4935
|
+
const key = toKey(message);
|
|
4936
|
+
if (seenMessageKeys.has(key)) continue;
|
|
4937
|
+
seenMessageKeys.add(key);
|
|
4938
|
+
collected.push(message);
|
|
2562
4939
|
}
|
|
2563
|
-
if (pagesRequested >= maxPages) {
|
|
4940
|
+
if (collected.length >= maxMessages || typedMessages.length === 0 || pagesRequested >= maxPages) {
|
|
2564
4941
|
finish();
|
|
2565
4942
|
return;
|
|
2566
4943
|
}
|
|
@@ -2598,8 +4975,70 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2598
4975
|
}
|
|
2599
4976
|
});
|
|
2600
4977
|
}
|
|
4978
|
+
function normalizeRecentMessageMentions(value) {
|
|
4979
|
+
if (!Array.isArray(value)) {
|
|
4980
|
+
return [];
|
|
4981
|
+
}
|
|
4982
|
+
const rows = [];
|
|
4983
|
+
const parseOptionalMentionInt = (input) => {
|
|
4984
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
4985
|
+
return Math.trunc(input);
|
|
4986
|
+
}
|
|
4987
|
+
if (typeof input === "string" && input.trim()) {
|
|
4988
|
+
const parsed = Number(input.trim());
|
|
4989
|
+
if (Number.isFinite(parsed)) {
|
|
4990
|
+
return Math.trunc(parsed);
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
return void 0;
|
|
4994
|
+
};
|
|
4995
|
+
for (const item of value) {
|
|
4996
|
+
if (!item || typeof item !== "object") continue;
|
|
4997
|
+
const record = item;
|
|
4998
|
+
const uid = normalizeCachedId(record.uid);
|
|
4999
|
+
if (!uid) continue;
|
|
5000
|
+
rows.push({
|
|
5001
|
+
uid,
|
|
5002
|
+
pos: parseOptionalMentionInt(record.pos),
|
|
5003
|
+
len: parseOptionalMentionInt(record.len),
|
|
5004
|
+
type: typeof record.type === "number" && Number.isFinite(record.type) ? Math.trunc(record.type) : typeof record.type === "string" && record.type.trim() ? Number.parseInt(record.type.trim(), 10) : void 0,
|
|
5005
|
+
rawJson: JSON.stringify(record)
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
return rows;
|
|
5009
|
+
}
|
|
5010
|
+
function toDbRecordFromRecentMessage(params) {
|
|
5011
|
+
const content = params.message.data?.content;
|
|
5012
|
+
const quote = params.message.data?.quote;
|
|
5013
|
+
return normalizeInboundListenRecord({
|
|
5014
|
+
profile: params.profile,
|
|
5015
|
+
threadType: params.message.type === ThreadType3.Group ? "group" : "user",
|
|
5016
|
+
rawThreadId: params.message.threadId,
|
|
5017
|
+
senderId: params.message.data?.uidFrom,
|
|
5018
|
+
senderName: params.message.data?.dName,
|
|
5019
|
+
toId: params.message.data?.idTo,
|
|
5020
|
+
selfId: params.selfId,
|
|
5021
|
+
title: params.title,
|
|
5022
|
+
msgId: params.message.data?.msgId,
|
|
5023
|
+
cliMsgId: params.message.data?.cliMsgId,
|
|
5024
|
+
actionId: params.message.data?.actionId,
|
|
5025
|
+
timestampMs: toEpochMs(params.message.data?.ts),
|
|
5026
|
+
msgType: params.message.data?.msgType,
|
|
5027
|
+
contentText: typeof content === "string" ? content : void 0,
|
|
5028
|
+
contentJson: content && typeof content === "object" ? JSON.stringify(content) : void 0,
|
|
5029
|
+
quoteMsgId: quote?.globalMsgId != null ? String(quote.globalMsgId) : void 0,
|
|
5030
|
+
quoteCliMsgId: quote?.cliMsgId != null ? String(quote.cliMsgId) : void 0,
|
|
5031
|
+
quoteOwnerId: quote?.ownerId != null ? String(quote.ownerId) : void 0,
|
|
5032
|
+
quoteText: typeof quote?.msg === "string" ? quote.msg : void 0,
|
|
5033
|
+
mentions: normalizeRecentMessageMentions(
|
|
5034
|
+
params.message.data?.mentions
|
|
5035
|
+
),
|
|
5036
|
+
rawMessage: params.message.data,
|
|
5037
|
+
source: params.source
|
|
5038
|
+
});
|
|
5039
|
+
}
|
|
2601
5040
|
async function parseCredentialFile(filePath) {
|
|
2602
|
-
const raw = await
|
|
5041
|
+
const raw = await fs6.readFile(filePath, "utf8");
|
|
2603
5042
|
const parsed = JSON.parse(raw);
|
|
2604
5043
|
if (!parsed.imei || !parsed.cookie || !parsed.userAgent) {
|
|
2605
5044
|
throw new Error("Credential file must include imei, cookie, and userAgent.");
|
|
@@ -2620,7 +5059,7 @@ async function waitForFileContent(filePath, timeoutMs) {
|
|
|
2620
5059
|
const startedAt = Date.now();
|
|
2621
5060
|
while (Date.now() - startedAt < timeoutMs) {
|
|
2622
5061
|
try {
|
|
2623
|
-
const data = await
|
|
5062
|
+
const data = await fs6.readFile(filePath);
|
|
2624
5063
|
if (data.length > 0) {
|
|
2625
5064
|
return data;
|
|
2626
5065
|
}
|
|
@@ -2635,8 +5074,8 @@ async function emitQrBase64FromDetachedLogin(profile, qrPath) {
|
|
|
2635
5074
|
if (!scriptPath) {
|
|
2636
5075
|
throw new Error("Cannot resolve CLI entrypoint for QR base64 mode.");
|
|
2637
5076
|
}
|
|
2638
|
-
const tempDir = await
|
|
2639
|
-
const targetPath =
|
|
5077
|
+
const tempDir = await fs6.mkdtemp(path6.join(os4.tmpdir(), "openzca-qr-"));
|
|
5078
|
+
const targetPath = path6.resolve(qrPath ?? path6.join(tempDir, "qr.png"));
|
|
2640
5079
|
const child = spawn2(
|
|
2641
5080
|
process.execPath,
|
|
2642
5081
|
[scriptPath, "--profile", profile, "auth", "login", "--qr-path", targetPath],
|
|
@@ -2914,7 +5353,7 @@ function mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind) {
|
|
|
2914
5353
|
if (fromType) return fromType;
|
|
2915
5354
|
try {
|
|
2916
5355
|
const parsedUrl = new URL(mediaUrl);
|
|
2917
|
-
const ext =
|
|
5356
|
+
const ext = path6.extname(parsedUrl.pathname);
|
|
2918
5357
|
if (ext) return ext;
|
|
2919
5358
|
} catch {
|
|
2920
5359
|
}
|
|
@@ -2945,20 +5384,20 @@ function parseInboundMediaFetchTimeoutMs() {
|
|
|
2945
5384
|
return Math.trunc(parsed);
|
|
2946
5385
|
}
|
|
2947
5386
|
function resolveOpenClawMediaDir() {
|
|
2948
|
-
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
2949
|
-
return
|
|
5387
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path6.join(os4.homedir(), ".openclaw");
|
|
5388
|
+
return path6.join(stateDir, "media");
|
|
2950
5389
|
}
|
|
2951
5390
|
function resolveInboundMediaDir(profile) {
|
|
2952
5391
|
const configuredRaw = process.env.OPENZCA_LISTEN_MEDIA_DIR?.trim();
|
|
2953
5392
|
if (configuredRaw) {
|
|
2954
5393
|
const configured = normalizeMediaInput(configuredRaw);
|
|
2955
|
-
return
|
|
5394
|
+
return path6.isAbsolute(configured) ? configured : path6.resolve(process.cwd(), configured);
|
|
2956
5395
|
}
|
|
2957
5396
|
const legacyRequested = process.env.OPENZCA_LISTEN_MEDIA_LEGACY_DIR?.trim() === "1";
|
|
2958
5397
|
if (legacyRequested) {
|
|
2959
|
-
return
|
|
5398
|
+
return path6.join(getProfileDir(profile), "inbound-media");
|
|
2960
5399
|
}
|
|
2961
|
-
return
|
|
5400
|
+
return path6.join(resolveOpenClawMediaDir(), "openzca", profile, "inbound");
|
|
2962
5401
|
}
|
|
2963
5402
|
async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
2964
5403
|
const maxBytes = parseMaxInboundMediaBytes();
|
|
@@ -2992,11 +5431,11 @@ async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
|
2992
5431
|
return null;
|
|
2993
5432
|
}
|
|
2994
5433
|
const dir = resolveInboundMediaDir(profile);
|
|
2995
|
-
await
|
|
5434
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
2996
5435
|
const ext = mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind);
|
|
2997
5436
|
const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
2998
|
-
const mediaPath =
|
|
2999
|
-
await
|
|
5437
|
+
const mediaPath = path6.join(dir, `${id}${ext}`);
|
|
5438
|
+
await fs6.writeFile(mediaPath, data);
|
|
3000
5439
|
return { mediaPath, mediaType };
|
|
3001
5440
|
}
|
|
3002
5441
|
async function cacheRemoteMediaEntries(params) {
|
|
@@ -3362,6 +5801,16 @@ function toEpochSeconds(input) {
|
|
|
3362
5801
|
}
|
|
3363
5802
|
return Math.floor(numeric);
|
|
3364
5803
|
}
|
|
5804
|
+
function toEpochMs(input) {
|
|
5805
|
+
const numeric = typeof input === "number" ? input : typeof input === "string" ? Number(input) : Number.NaN;
|
|
5806
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
5807
|
+
return Date.now();
|
|
5808
|
+
}
|
|
5809
|
+
if (numeric < 1e10) {
|
|
5810
|
+
return Math.floor(numeric * 1e3);
|
|
5811
|
+
}
|
|
5812
|
+
return Math.floor(numeric);
|
|
5813
|
+
}
|
|
3365
5814
|
function parseNonNegativeIntOption(label, value) {
|
|
3366
5815
|
if (!value || !value.trim()) return void 0;
|
|
3367
5816
|
const parsed = Number(value.trim());
|
|
@@ -3370,6 +5819,14 @@ function parseNonNegativeIntOption(label, value) {
|
|
|
3370
5819
|
}
|
|
3371
5820
|
return Math.trunc(parsed);
|
|
3372
5821
|
}
|
|
5822
|
+
function parsePositiveIntOption(label, value) {
|
|
5823
|
+
if (!value || !value.trim()) return void 0;
|
|
5824
|
+
const parsed = Number(value.trim());
|
|
5825
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
5826
|
+
throw new Error(`${label} must be a positive number.`);
|
|
5827
|
+
}
|
|
5828
|
+
return Math.trunc(parsed);
|
|
5829
|
+
}
|
|
3373
5830
|
program.name("openzca").description("Open-source zca-cli compatible wrapper powered by zca-js").version(PKG_VERSION).option("-p, --profile <name>", "Profile name").option("--debug", "Enable debug logging").option("--debug-file <path>", "Debug log file path").showHelpAfterError();
|
|
3374
5831
|
program.hook("preAction", (_parent, actionCommand) => {
|
|
3375
5832
|
if (!resolveDebugEnabled(actionCommand)) {
|
|
@@ -3488,7 +5945,7 @@ auth.command("login").description("Login with QR code").option("-q, --qr-path <p
|
|
|
3488
5945
|
auth.command("login-cred [file]").alias("login-creds").description("Login using credential JSON file").action(
|
|
3489
5946
|
wrapAction(async (file, command) => {
|
|
3490
5947
|
const profile = await currentProfile(command);
|
|
3491
|
-
const credentials = file ? await parseCredentialFile(
|
|
5948
|
+
const credentials = file ? await parseCredentialFile(path6.resolve(normalizeMediaInput(file))) : toCredentials(
|
|
3492
5949
|
await loadCredentials(profile) ?? (() => {
|
|
3493
5950
|
throw new Error(
|
|
3494
5951
|
`No saved credentials for profile "${profile}". Run: openzca auth login`
|
|
@@ -3561,26 +6018,397 @@ auth.command("cache-clear").description("Clear local cache").action(
|
|
|
3561
6018
|
console.log(`Cache cleared for profile ${profile}`);
|
|
3562
6019
|
})
|
|
3563
6020
|
);
|
|
6021
|
+
var dbCmd = program.command("db").description("Profile-scoped SQLite message database");
|
|
6022
|
+
dbCmd.command("enable").option("--path <path>", "Custom SQLite file path").description("Enable local SQLite persistence for the active profile").action(
|
|
6023
|
+
wrapAction(async (opts, command) => {
|
|
6024
|
+
const profile = await currentProfile(command);
|
|
6025
|
+
await enableDb(profile, opts.path);
|
|
6026
|
+
output(await getDbStatus(profile), false);
|
|
6027
|
+
})
|
|
6028
|
+
);
|
|
6029
|
+
dbCmd.command("disable").description("Disable automatic SQLite persistence for the active profile").action(
|
|
6030
|
+
wrapAction(async (command) => {
|
|
6031
|
+
const profile = await currentProfile(command);
|
|
6032
|
+
await disableDb(profile);
|
|
6033
|
+
await closeDb(profile);
|
|
6034
|
+
output(await getDbStatus(profile), false);
|
|
6035
|
+
})
|
|
6036
|
+
);
|
|
6037
|
+
dbCmd.command("reset").option("-y, --yes", "Delete the SQLite DB file for the active profile").option("--drop-config", "Also remove the DB config file").option("-j, --json", "JSON output").description("Delete the local SQLite DB for the active profile").action(
|
|
6038
|
+
wrapAction(async (opts, command) => {
|
|
6039
|
+
if (!opts.yes) {
|
|
6040
|
+
const confirmed = await confirmDestructiveAction(
|
|
6041
|
+
"Reset the local SQLite DB for the active profile?"
|
|
6042
|
+
);
|
|
6043
|
+
if (!confirmed) {
|
|
6044
|
+
console.log("Cancelled.");
|
|
6045
|
+
return;
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
const profile = await currentProfile(command);
|
|
6049
|
+
const dbPath = await resolveDbPath(profile);
|
|
6050
|
+
const configPath = getDbConfigPath(profile);
|
|
6051
|
+
await closeDb(profile);
|
|
6052
|
+
const removedPaths = [];
|
|
6053
|
+
const deleteIfExists = async (filename) => {
|
|
6054
|
+
try {
|
|
6055
|
+
await fs6.unlink(filename);
|
|
6056
|
+
removedPaths.push(filename);
|
|
6057
|
+
} catch (error) {
|
|
6058
|
+
if (error.code !== "ENOENT") {
|
|
6059
|
+
throw error;
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
};
|
|
6063
|
+
await deleteIfExists(dbPath);
|
|
6064
|
+
await deleteIfExists(`${dbPath}-wal`);
|
|
6065
|
+
await deleteIfExists(`${dbPath}-shm`);
|
|
6066
|
+
if (opts.dropConfig) {
|
|
6067
|
+
await deleteIfExists(configPath);
|
|
6068
|
+
}
|
|
6069
|
+
const status = await getDbStatus(profile);
|
|
6070
|
+
output(
|
|
6071
|
+
{
|
|
6072
|
+
profile,
|
|
6073
|
+
removedPaths,
|
|
6074
|
+
droppedConfig: Boolean(opts.dropConfig),
|
|
6075
|
+
status: {
|
|
6076
|
+
enabled: status.enabled,
|
|
6077
|
+
path: status.path,
|
|
6078
|
+
exists: status.exists,
|
|
6079
|
+
messageCount: status.messageCount,
|
|
6080
|
+
threadCount: status.threadCount,
|
|
6081
|
+
groupCount: status.groupCount,
|
|
6082
|
+
userCount: status.userCount
|
|
6083
|
+
}
|
|
6084
|
+
},
|
|
6085
|
+
Boolean(opts.json)
|
|
6086
|
+
);
|
|
6087
|
+
})
|
|
6088
|
+
);
|
|
6089
|
+
dbCmd.command("status").option("-j, --json", "JSON output").description("Show DB status for the active profile").action(
|
|
6090
|
+
wrapAction(async (opts, command) => {
|
|
6091
|
+
const profile = await currentProfile(command);
|
|
6092
|
+
const config = await readDbConfig(profile);
|
|
6093
|
+
const status = await getDbStatus(profile);
|
|
6094
|
+
const syncRows = await listSyncState({ profile });
|
|
6095
|
+
output(
|
|
6096
|
+
{
|
|
6097
|
+
profile,
|
|
6098
|
+
enabled: status.enabled,
|
|
6099
|
+
path: await resolveDbPath(profile),
|
|
6100
|
+
exists: status.exists,
|
|
6101
|
+
configuredPath: config.path ?? null,
|
|
6102
|
+
messageCount: status.messageCount,
|
|
6103
|
+
threadCount: status.threadCount,
|
|
6104
|
+
groupCount: status.groupCount,
|
|
6105
|
+
userCount: status.userCount,
|
|
6106
|
+
syncStates: {
|
|
6107
|
+
total: syncRows.length,
|
|
6108
|
+
synced: syncRows.filter((row) => row.status === "synced").length,
|
|
6109
|
+
errors: syncRows.filter((row) => row.status === "error").length
|
|
6110
|
+
},
|
|
6111
|
+
lastMessageAtMs: status.lastMessageAtMs ?? null,
|
|
6112
|
+
updatedAt: status.updatedAt ?? null
|
|
6113
|
+
},
|
|
6114
|
+
Boolean(opts.json)
|
|
6115
|
+
);
|
|
6116
|
+
})
|
|
6117
|
+
);
|
|
6118
|
+
var dbMe = dbCmd.command("me").description("Query stored self profile data");
|
|
6119
|
+
dbMe.command("info").option("-j, --json", "JSON output").description("Show stored self profile info").action(
|
|
6120
|
+
wrapAction(async (opts, command) => {
|
|
6121
|
+
const profile = await currentProfile(command);
|
|
6122
|
+
const row = await getSelfProfile(profile);
|
|
6123
|
+
if (!row?.info) {
|
|
6124
|
+
throw new Error("No stored self profile in DB. Run `openzca db sync` first.");
|
|
6125
|
+
}
|
|
6126
|
+
output(row.info, Boolean(opts.json));
|
|
6127
|
+
})
|
|
6128
|
+
);
|
|
6129
|
+
dbMe.command("id").description("Show stored self user ID").action(
|
|
6130
|
+
wrapAction(async (command) => {
|
|
6131
|
+
const profile = await currentProfile(command);
|
|
6132
|
+
const row = await getSelfProfile(profile);
|
|
6133
|
+
if (!row?.userId) {
|
|
6134
|
+
throw new Error("No stored self profile in DB. Run `openzca db sync` first.");
|
|
6135
|
+
}
|
|
6136
|
+
console.log(row.userId);
|
|
6137
|
+
})
|
|
6138
|
+
);
|
|
6139
|
+
var dbGroup = dbCmd.command("group").description("Query stored group data");
|
|
6140
|
+
dbGroup.command("list").option("-j, --json", "JSON output").description("List groups stored in the local DB").action(
|
|
6141
|
+
wrapAction(async (opts, command) => {
|
|
6142
|
+
const profile = await currentProfile(command);
|
|
6143
|
+
output(await listGroups(profile), Boolean(opts.json));
|
|
6144
|
+
})
|
|
6145
|
+
);
|
|
6146
|
+
dbGroup.command("info <groupId>").option("-j, --json", "JSON output").description("Show stored info for a group").action(
|
|
6147
|
+
wrapAction(async (groupId, opts, command) => {
|
|
6148
|
+
const profile = await currentProfile(command);
|
|
6149
|
+
const row = await getThreadInfo({ profile, threadId: groupId, threadType: "group" });
|
|
6150
|
+
if (!row) {
|
|
6151
|
+
throw new Error(`Group not found in DB: ${groupId}`);
|
|
6152
|
+
}
|
|
6153
|
+
output(row, Boolean(opts.json));
|
|
6154
|
+
})
|
|
6155
|
+
);
|
|
6156
|
+
dbGroup.command("members <groupId>").option("-j, --json", "JSON output").description("List stored members for a group").action(
|
|
6157
|
+
wrapAction(async (groupId, opts, command) => {
|
|
6158
|
+
const profile = await currentProfile(command);
|
|
6159
|
+
output(await listThreadMembers({ profile, threadId: groupId }), Boolean(opts.json));
|
|
6160
|
+
})
|
|
6161
|
+
);
|
|
6162
|
+
dbGroup.command("messages <groupId>").option("--since <duration>", "Rolling window ending now: duration like 30s, 7m, 24h, 7d, or 2w").option("--from <time>", "Lower time bound: ISO timestamp, date, or unix seconds/ms").option("--until <time>", "Upper time bound: ISO timestamp, date, or unix seconds/ms").option("--to <time>", "Alias for --until").option("--limit <count>", "Maximum number of rows").option("--all", "Return all matching rows").option("--oldest-first", "Sort oldest-first instead of newest-first").option("-j, --json", "JSON output").description("List stored messages for a group").action(
|
|
6163
|
+
wrapAction(async (groupId, opts, command) => {
|
|
6164
|
+
const profile = await currentProfile(command);
|
|
6165
|
+
const { sinceMs, untilMs, limit, newestFirst } = resolveMessageQueryOptions(opts);
|
|
6166
|
+
const rows = await listMessages({
|
|
6167
|
+
profile,
|
|
6168
|
+
threadId: groupId,
|
|
6169
|
+
threadType: "group",
|
|
6170
|
+
sinceMs,
|
|
6171
|
+
untilMs,
|
|
6172
|
+
limit,
|
|
6173
|
+
newestFirst
|
|
6174
|
+
});
|
|
6175
|
+
output(
|
|
6176
|
+
{
|
|
6177
|
+
groupId,
|
|
6178
|
+
count: rows.length,
|
|
6179
|
+
messages: rows
|
|
6180
|
+
},
|
|
6181
|
+
Boolean(opts.json)
|
|
6182
|
+
);
|
|
6183
|
+
})
|
|
6184
|
+
);
|
|
6185
|
+
var dbFriend = dbCmd.command("friend").description("Query stored friend directory data");
|
|
6186
|
+
dbFriend.command("list").option("-j, --json", "JSON output").description("List friends stored in the local DB").action(
|
|
6187
|
+
wrapAction(async (opts, command) => {
|
|
6188
|
+
const profile = await currentProfile(command);
|
|
6189
|
+
output(await listFriends(profile), Boolean(opts.json));
|
|
6190
|
+
})
|
|
6191
|
+
);
|
|
6192
|
+
dbFriend.command("find <query>").option("-j, --json", "JSON output").description("Find stored friends by ID or name").action(
|
|
6193
|
+
wrapAction(async (query, opts, command) => {
|
|
6194
|
+
const profile = await currentProfile(command);
|
|
6195
|
+
output(await findFriends({ profile, query }), Boolean(opts.json));
|
|
6196
|
+
})
|
|
6197
|
+
);
|
|
6198
|
+
dbFriend.command("info <userId>").option("-j, --json", "JSON output").description("Show stored info for a friend").action(
|
|
6199
|
+
wrapAction(async (userId, opts, command) => {
|
|
6200
|
+
const profile = await currentProfile(command);
|
|
6201
|
+
const row = await getFriendInfo({ profile, userId });
|
|
6202
|
+
if (!row) {
|
|
6203
|
+
throw new Error(`Friend not found in DB: ${userId}`);
|
|
6204
|
+
}
|
|
6205
|
+
output(row, Boolean(opts.json));
|
|
6206
|
+
})
|
|
6207
|
+
);
|
|
6208
|
+
dbFriend.command("messages <userId>").option("--since <duration>", "Rolling window ending now: duration like 30s, 7m, 24h, 7d, or 2w").option("--from <time>", "Lower time bound: ISO timestamp, date, or unix seconds/ms").option("--until <time>", "Upper time bound: ISO timestamp, date, or unix seconds/ms").option("--to <time>", "Alias for --until").option("--limit <count>", "Maximum number of rows").option("--all", "Return all matching rows").option("--oldest-first", "Sort oldest-first instead of newest-first").option("-j, --json", "JSON output").description("List stored direct-message rows for a friend").action(
|
|
6209
|
+
wrapAction(async (userId, opts, command) => {
|
|
6210
|
+
const profile = await currentProfile(command);
|
|
6211
|
+
const { sinceMs, untilMs, limit, newestFirst } = resolveMessageQueryOptions(opts);
|
|
6212
|
+
const rows = await listMessages({
|
|
6213
|
+
profile,
|
|
6214
|
+
threadId: userId,
|
|
6215
|
+
threadType: "user",
|
|
6216
|
+
sinceMs,
|
|
6217
|
+
untilMs,
|
|
6218
|
+
limit,
|
|
6219
|
+
newestFirst
|
|
6220
|
+
});
|
|
6221
|
+
output(
|
|
6222
|
+
{
|
|
6223
|
+
userId,
|
|
6224
|
+
count: rows.length,
|
|
6225
|
+
messages: rows
|
|
6226
|
+
},
|
|
6227
|
+
Boolean(opts.json)
|
|
6228
|
+
);
|
|
6229
|
+
})
|
|
6230
|
+
);
|
|
6231
|
+
var dbChat = dbCmd.command("chat").description("Query stored conversation data");
|
|
6232
|
+
dbChat.command("list").option("-j, --json", "JSON output").description("List chats stored in the local DB").action(
|
|
6233
|
+
wrapAction(async (opts, command) => {
|
|
6234
|
+
const profile = await currentProfile(command);
|
|
6235
|
+
output(await listChats(profile), shouldOutputJson(opts));
|
|
6236
|
+
})
|
|
6237
|
+
);
|
|
6238
|
+
dbChat.command("info <chatId>").option("-g, --group", "Read as a group chat").option("-j, --json", "JSON output").description("Show stored info for a chat").action(
|
|
6239
|
+
wrapAction(async (chatId, opts, command) => {
|
|
6240
|
+
const profile = await currentProfile(command);
|
|
6241
|
+
const row = await getThreadInfo({
|
|
6242
|
+
profile,
|
|
6243
|
+
threadId: chatId,
|
|
6244
|
+
threadType: opts.group ? "group" : void 0
|
|
6245
|
+
});
|
|
6246
|
+
if (!row) {
|
|
6247
|
+
throw new Error(`Chat not found in DB: ${chatId}`);
|
|
6248
|
+
}
|
|
6249
|
+
output(row, shouldOutputJson(opts));
|
|
6250
|
+
})
|
|
6251
|
+
);
|
|
6252
|
+
dbChat.command("messages <chatId>").option("-g, --group", "Read as a group chat").option("--since <duration>", "Rolling window ending now: duration like 30s, 7m, 24h, 7d, or 2w").option("--from <time>", "Lower time bound: ISO timestamp, date, or unix seconds/ms").option("--until <time>", "Upper time bound: ISO timestamp, date, or unix seconds/ms").option("--to <time>", "Alias for --until").option("--limit <count>", "Maximum number of rows").option("--all", "Return all matching rows").option("--oldest-first", "Sort oldest-first instead of newest-first").option("-j, --json", "JSON output").description("List stored messages for a chat").action(
|
|
6253
|
+
wrapAction(async (chatId, opts, command) => {
|
|
6254
|
+
const profile = await currentProfile(command);
|
|
6255
|
+
const threadType = await resolveStoredChatThreadType(profile, chatId, opts.group);
|
|
6256
|
+
const { sinceMs, untilMs, limit, newestFirst } = resolveMessageQueryOptions(opts);
|
|
6257
|
+
const rows = await listMessages({
|
|
6258
|
+
profile,
|
|
6259
|
+
threadId: chatId,
|
|
6260
|
+
threadType,
|
|
6261
|
+
sinceMs,
|
|
6262
|
+
untilMs,
|
|
6263
|
+
limit,
|
|
6264
|
+
newestFirst
|
|
6265
|
+
});
|
|
6266
|
+
output(
|
|
6267
|
+
{
|
|
6268
|
+
chatId,
|
|
6269
|
+
threadType,
|
|
6270
|
+
count: rows.length,
|
|
6271
|
+
messages: rows
|
|
6272
|
+
},
|
|
6273
|
+
shouldOutputJson(opts)
|
|
6274
|
+
);
|
|
6275
|
+
})
|
|
6276
|
+
);
|
|
6277
|
+
var dbMessage = dbCmd.command("message").description("Query stored messages");
|
|
6278
|
+
dbMessage.command("get <id>").option("-j, --json", "JSON output").description("Read one stored message by msgId, cliMsgId, or internal uid").action(
|
|
6279
|
+
wrapAction(async (id, opts, command) => {
|
|
6280
|
+
const profile = await currentProfile(command);
|
|
6281
|
+
const row = await getMessageById({ profile, id });
|
|
6282
|
+
if (!row) {
|
|
6283
|
+
throw new Error(`Message not found in DB: ${id}`);
|
|
6284
|
+
}
|
|
6285
|
+
output(row, Boolean(opts.json));
|
|
6286
|
+
})
|
|
6287
|
+
);
|
|
6288
|
+
var dbSync = dbCmd.command("sync").description("Sync discoverable data into the local DB");
|
|
6289
|
+
dbSync.enablePositionalOptions();
|
|
6290
|
+
dbSync.option("-n, --count <count>", "Recent DM/chat messages to fetch per window", "200").option("-j, --json", "JSON output").action(
|
|
6291
|
+
wrapAction(async (opts, command) => {
|
|
6292
|
+
const count = resolveSyncWindowCount(opts.count);
|
|
6293
|
+
const progress = createSyncProgressReporter();
|
|
6294
|
+
const summary = await runDbSync({
|
|
6295
|
+
command,
|
|
6296
|
+
mode: "all",
|
|
6297
|
+
count,
|
|
6298
|
+
progress
|
|
6299
|
+
});
|
|
6300
|
+
output(summary, Boolean(opts.json));
|
|
6301
|
+
})
|
|
6302
|
+
);
|
|
6303
|
+
dbSync.command("all").option("-n, --count <count>", "Recent DM/chat messages to fetch per window", "200").option("-j, --json", "JSON output").description("Sync full group history, friend directory, and recent DM/chat windows").action(
|
|
6304
|
+
wrapAction(async (_opts, command) => {
|
|
6305
|
+
const count = resolveSyncWindowCount(readCliOptionValue(["--count", "-n"]));
|
|
6306
|
+
output(
|
|
6307
|
+
await runDbSync({ command, mode: "all", count, progress: createSyncProgressReporter() }),
|
|
6308
|
+
readCliFlag(["--json", "-j"])
|
|
6309
|
+
);
|
|
6310
|
+
})
|
|
6311
|
+
);
|
|
6312
|
+
dbSync.command("groups").option("-j, --json", "JSON output").description("Sync group directory, members, and full group history").action(
|
|
6313
|
+
wrapAction(async (_opts, command) => {
|
|
6314
|
+
output(
|
|
6315
|
+
await runDbSync({ command, mode: "groups", count: 0, progress: createSyncProgressReporter() }),
|
|
6316
|
+
readCliFlag(["--json", "-j"])
|
|
6317
|
+
);
|
|
6318
|
+
})
|
|
6319
|
+
);
|
|
6320
|
+
dbSync.command("friends").option("-j, --json", "JSON output").description("Sync friend directory only").action(
|
|
6321
|
+
wrapAction(async (_opts, command) => {
|
|
6322
|
+
output(
|
|
6323
|
+
await runDbSync({ command, mode: "friends", count: 0, progress: createSyncProgressReporter() }),
|
|
6324
|
+
readCliFlag(["--json", "-j"])
|
|
6325
|
+
);
|
|
6326
|
+
})
|
|
6327
|
+
);
|
|
6328
|
+
dbSync.command("chats").option("-n, --count <count>", "Recent messages to fetch per scan/window", "200").option("-j, --json", "JSON output").description("Sync discoverable chat windows (DM/chat sync is best-effort)").action(
|
|
6329
|
+
wrapAction(async (_opts, command) => {
|
|
6330
|
+
const count = resolveSyncWindowCount(readCliOptionValue(["--count", "-n"]));
|
|
6331
|
+
output(
|
|
6332
|
+
await runDbSync({ command, mode: "chats", count, progress: createSyncProgressReporter() }),
|
|
6333
|
+
readCliFlag(["--json", "-j"])
|
|
6334
|
+
);
|
|
6335
|
+
})
|
|
6336
|
+
);
|
|
6337
|
+
dbSync.command("group <groupId>").option("-j, --json", "JSON output").description("Sync one group with full group history").action(
|
|
6338
|
+
wrapAction(async (groupId, _opts, command) => {
|
|
6339
|
+
output(
|
|
6340
|
+
await runDbSync({
|
|
6341
|
+
command,
|
|
6342
|
+
mode: "group",
|
|
6343
|
+
count: 0,
|
|
6344
|
+
groupId,
|
|
6345
|
+
progress: createSyncProgressReporter()
|
|
6346
|
+
}),
|
|
6347
|
+
readCliFlag(["--json", "-j"])
|
|
6348
|
+
);
|
|
6349
|
+
})
|
|
6350
|
+
);
|
|
6351
|
+
dbSync.command("chat <chatId>").option("-n, --count <count>", "Recent messages to fetch for this chat", "200").option("-j, --json", "JSON output").description("Sync one chat (best-effort for direct-message history)").action(
|
|
6352
|
+
wrapAction(async (chatId, _opts, command) => {
|
|
6353
|
+
const count = resolveSyncWindowCount(readCliOptionValue(["--count", "-n"]));
|
|
6354
|
+
output(
|
|
6355
|
+
await runDbSync({
|
|
6356
|
+
command,
|
|
6357
|
+
mode: "chat",
|
|
6358
|
+
count,
|
|
6359
|
+
threadId: chatId,
|
|
6360
|
+
progress: createSyncProgressReporter()
|
|
6361
|
+
}),
|
|
6362
|
+
readCliFlag(["--json", "-j"])
|
|
6363
|
+
);
|
|
6364
|
+
})
|
|
6365
|
+
);
|
|
3564
6366
|
var msg = program.command("msg").description("Messaging commands");
|
|
3565
|
-
msg.command("send <threadId> <message>").option("-g, --group", "Send to group").option("--raw", "Send raw text without parsing formatting markers").description("Send text message with formatting (**bold** *italic* __bold__ ~~strike~~ {underline}text{/underline} {red}color{/red} {big}size{/big} lists indents). Group sends also resolve unique @Name/@userId mentions.").action(
|
|
6367
|
+
msg.command("send <threadId> <message>").option("-g, --group", "Send to group").option("--raw", "Send raw text without parsing formatting markers").option("--reply-id <id>", "Reply using a stored DB message id/msgId/cliMsgId").option("--reply-message <json>", "Reply using a raw message.data JSON object").description("Send text message with formatting (**bold** *italic* __bold__ ~~strike~~ {underline}text{/underline} {red}color{/red} {big}size{/big} lists indents). Group sends also resolve unique @Name/@userId mentions.").action(
|
|
3566
6368
|
wrapAction(async (threadId, message, opts, command) => {
|
|
3567
|
-
const { api } = await requireApi(command);
|
|
6369
|
+
const { api, profile } = await requireApi(command);
|
|
3568
6370
|
const threadType = asThreadType(opts.group);
|
|
3569
|
-
const
|
|
6371
|
+
const textPayload = await buildTextSendPayload({
|
|
3570
6372
|
message,
|
|
3571
6373
|
raw: opts.raw,
|
|
3572
6374
|
threadType,
|
|
3573
6375
|
threadId,
|
|
3574
|
-
listGroupMembers: threadType ===
|
|
6376
|
+
listGroupMembers: threadType === ThreadType3.Group ? (groupId) => listGroupMentionMembers(api, groupId) : void 0
|
|
3575
6377
|
});
|
|
6378
|
+
const quote = await resolveSendReplyQuote({
|
|
6379
|
+
profile,
|
|
6380
|
+
api,
|
|
6381
|
+
threadId,
|
|
6382
|
+
threadType,
|
|
6383
|
+
replyId: opts.replyId,
|
|
6384
|
+
replyMessage: opts.replyMessage
|
|
6385
|
+
});
|
|
6386
|
+
const payload = quote || typeof textPayload !== "string" ? {
|
|
6387
|
+
...typeof textPayload === "string" ? { msg: textPayload } : textPayload,
|
|
6388
|
+
...quote ? { quote } : {}
|
|
6389
|
+
} : textPayload;
|
|
3576
6390
|
const response = await api.sendMessage(payload, threadId, threadType);
|
|
3577
6391
|
output(response, false);
|
|
6392
|
+
if (await shouldWriteToDb(profile)) {
|
|
6393
|
+
scheduleDbWrite(profile, command, "msg.send.db.persist_error", async () => {
|
|
6394
|
+
await persistOutgoingMessageBestEffort({
|
|
6395
|
+
profile,
|
|
6396
|
+
api,
|
|
6397
|
+
threadId,
|
|
6398
|
+
group: opts.group,
|
|
6399
|
+
text: message,
|
|
6400
|
+
msgType: "text",
|
|
6401
|
+
response,
|
|
6402
|
+
rawPayload: payload
|
|
6403
|
+
});
|
|
6404
|
+
});
|
|
6405
|
+
}
|
|
3578
6406
|
})
|
|
3579
6407
|
);
|
|
3580
6408
|
msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (repeatable)", collectValues, []).option("-m, --message <message>", "Caption").option("-g, --group", "Send to group").description("Send image(s) from file or URL").action(
|
|
3581
6409
|
wrapAction(
|
|
3582
6410
|
async (threadId, file, opts, command) => {
|
|
3583
|
-
const { api } = await requireApi(command);
|
|
6411
|
+
const { api, profile } = await requireApi(command);
|
|
3584
6412
|
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
3585
6413
|
const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
|
|
3586
6414
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
@@ -3611,6 +6439,28 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
|
|
|
3611
6439
|
asThreadType(opts.group)
|
|
3612
6440
|
);
|
|
3613
6441
|
output(response, false);
|
|
6442
|
+
if (await shouldWriteToDb(profile)) {
|
|
6443
|
+
scheduleDbWrite(profile, command, "msg.image.db.persist_error", async () => {
|
|
6444
|
+
await persistOutgoingMessageBestEffort({
|
|
6445
|
+
profile,
|
|
6446
|
+
api,
|
|
6447
|
+
threadId,
|
|
6448
|
+
group: opts.group,
|
|
6449
|
+
text: opts.message ?? "",
|
|
6450
|
+
msgType: "image",
|
|
6451
|
+
response,
|
|
6452
|
+
rawPayload: {
|
|
6453
|
+
msg: opts.message ?? "",
|
|
6454
|
+
attachments
|
|
6455
|
+
},
|
|
6456
|
+
media: attachments.map((item) => ({
|
|
6457
|
+
mediaKind: "image",
|
|
6458
|
+
mediaPath: isHttpUrl(item) ? void 0 : item,
|
|
6459
|
+
mediaUrl: isHttpUrl(item) ? item : void 0
|
|
6460
|
+
}))
|
|
6461
|
+
});
|
|
6462
|
+
});
|
|
6463
|
+
}
|
|
3614
6464
|
} finally {
|
|
3615
6465
|
await downloaded.cleanup();
|
|
3616
6466
|
}
|
|
@@ -3691,6 +6541,30 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
|
|
|
3691
6541
|
command
|
|
3692
6542
|
);
|
|
3693
6543
|
output(response2, false);
|
|
6544
|
+
if (await shouldWriteToDb(profile)) {
|
|
6545
|
+
scheduleDbWrite(profile, command, "msg.video.db.persist_error", async () => {
|
|
6546
|
+
await persistOutgoingMessageBestEffort({
|
|
6547
|
+
profile,
|
|
6548
|
+
api,
|
|
6549
|
+
threadId,
|
|
6550
|
+
group: opts.group,
|
|
6551
|
+
text: opts.message ?? "",
|
|
6552
|
+
msgType: "video",
|
|
6553
|
+
response: response2,
|
|
6554
|
+
rawPayload: {
|
|
6555
|
+
msg: opts.message ?? "",
|
|
6556
|
+
videoPath: attachments[0],
|
|
6557
|
+
thumbnailPath: thumbnailPath ?? null
|
|
6558
|
+
},
|
|
6559
|
+
media: [
|
|
6560
|
+
{
|
|
6561
|
+
mediaKind: "video",
|
|
6562
|
+
mediaPath: attachments[0]
|
|
6563
|
+
}
|
|
6564
|
+
]
|
|
6565
|
+
});
|
|
6566
|
+
});
|
|
6567
|
+
}
|
|
3694
6568
|
return;
|
|
3695
6569
|
} catch (error) {
|
|
3696
6570
|
writeDebugLine(
|
|
@@ -3729,6 +6603,28 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
|
|
|
3729
6603
|
)
|
|
3730
6604
|
);
|
|
3731
6605
|
output(response, false);
|
|
6606
|
+
if (await shouldWriteToDb(profile)) {
|
|
6607
|
+
scheduleDbWrite(profile, command, "msg.video.db.persist_error", async () => {
|
|
6608
|
+
await persistOutgoingMessageBestEffort({
|
|
6609
|
+
profile,
|
|
6610
|
+
api,
|
|
6611
|
+
threadId,
|
|
6612
|
+
group: opts.group,
|
|
6613
|
+
text: opts.message ?? "",
|
|
6614
|
+
msgType: "video",
|
|
6615
|
+
response,
|
|
6616
|
+
rawPayload: {
|
|
6617
|
+
msg: opts.message ?? "",
|
|
6618
|
+
attachments
|
|
6619
|
+
},
|
|
6620
|
+
media: attachments.map((item) => ({
|
|
6621
|
+
mediaKind: "video",
|
|
6622
|
+
mediaPath: isHttpUrl(item) ? void 0 : item,
|
|
6623
|
+
mediaUrl: isHttpUrl(item) ? item : void 0
|
|
6624
|
+
}))
|
|
6625
|
+
});
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
3732
6628
|
} finally {
|
|
3733
6629
|
await downloaded.cleanup();
|
|
3734
6630
|
await downloadedThumbnail.cleanup();
|
|
@@ -3739,7 +6635,7 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
|
|
|
3739
6635
|
msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (repeatable)", collectValues, []).option("-g, --group", "Send to group").description("Send voice message from file or URL").action(
|
|
3740
6636
|
wrapAction(
|
|
3741
6637
|
async (threadId, file, opts, command) => {
|
|
3742
|
-
const { api } = await requireApi(command);
|
|
6638
|
+
const { api, profile } = await requireApi(command);
|
|
3743
6639
|
const type = asThreadType(opts.group);
|
|
3744
6640
|
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
3745
6641
|
const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
|
|
@@ -3775,6 +6671,24 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
|
|
|
3775
6671
|
);
|
|
3776
6672
|
}
|
|
3777
6673
|
output(results, false);
|
|
6674
|
+
if (await shouldWriteToDb(profile)) {
|
|
6675
|
+
scheduleDbWrite(profile, command, "msg.voice.db.persist_error", async () => {
|
|
6676
|
+
await persistOutgoingMessageBestEffort({
|
|
6677
|
+
profile,
|
|
6678
|
+
api,
|
|
6679
|
+
threadId,
|
|
6680
|
+
group: opts.group,
|
|
6681
|
+
msgType: "voice",
|
|
6682
|
+
response: results,
|
|
6683
|
+
rawPayload: uploaded,
|
|
6684
|
+
media: uploaded.map((item) => ({
|
|
6685
|
+
mediaKind: "voice",
|
|
6686
|
+
mediaUrl: "fileUrl" in item && typeof item.fileUrl === "string" ? item.fileUrl : void 0,
|
|
6687
|
+
rawJson: JSON.stringify(item)
|
|
6688
|
+
}))
|
|
6689
|
+
});
|
|
6690
|
+
});
|
|
6691
|
+
}
|
|
3778
6692
|
} finally {
|
|
3779
6693
|
await downloaded.cleanup();
|
|
3780
6694
|
}
|
|
@@ -3805,9 +6719,23 @@ msg.command("sticker <threadId> <stickerId>").option("-g, --group", "Send to gro
|
|
|
3805
6719
|
);
|
|
3806
6720
|
msg.command("link <threadId> <url>").option("-g, --group", "Send to group").description("Send link").action(
|
|
3807
6721
|
wrapAction(async (threadId, url, opts, command) => {
|
|
3808
|
-
const { api } = await requireApi(command);
|
|
6722
|
+
const { api, profile } = await requireApi(command);
|
|
3809
6723
|
const response = await api.sendLink({ link: url }, threadId, asThreadType(opts.group));
|
|
3810
6724
|
output(response, false);
|
|
6725
|
+
if (await shouldWriteToDb(profile)) {
|
|
6726
|
+
scheduleDbWrite(profile, command, "msg.link.db.persist_error", async () => {
|
|
6727
|
+
await persistOutgoingMessageBestEffort({
|
|
6728
|
+
profile,
|
|
6729
|
+
api,
|
|
6730
|
+
threadId,
|
|
6731
|
+
group: opts.group,
|
|
6732
|
+
text: url,
|
|
6733
|
+
msgType: "link",
|
|
6734
|
+
response,
|
|
6735
|
+
rawPayload: { link: url }
|
|
6736
|
+
});
|
|
6737
|
+
});
|
|
6738
|
+
}
|
|
3811
6739
|
})
|
|
3812
6740
|
);
|
|
3813
6741
|
msg.command("card <threadId> <contactId>").option("-g, --group", "Send to group").description("Send contact card").action(
|
|
@@ -3947,8 +6875,8 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3947
6875
|
{
|
|
3948
6876
|
threadId,
|
|
3949
6877
|
explicitGroupFlag: Boolean(opts.group),
|
|
3950
|
-
isGroup: threadResolution.type ===
|
|
3951
|
-
threadType: threadResolution.type ===
|
|
6878
|
+
isGroup: threadResolution.type === ThreadType3.Group,
|
|
6879
|
+
threadType: threadResolution.type === ThreadType3.Group ? "group" : "user",
|
|
3952
6880
|
threadTypeReason: threadResolution.reason,
|
|
3953
6881
|
localFiles,
|
|
3954
6882
|
urlInputs
|
|
@@ -3976,7 +6904,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3976
6904
|
"msg.upload.ipc.done",
|
|
3977
6905
|
{
|
|
3978
6906
|
threadId,
|
|
3979
|
-
threadType: threadResolution.type ===
|
|
6907
|
+
threadType: threadResolution.type === ThreadType3.Group ? "group" : "user"
|
|
3980
6908
|
},
|
|
3981
6909
|
command
|
|
3982
6910
|
);
|
|
@@ -3987,7 +6915,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3987
6915
|
"msg.upload.ipc.fallback",
|
|
3988
6916
|
{
|
|
3989
6917
|
threadId,
|
|
3990
|
-
threadType: threadResolution.type ===
|
|
6918
|
+
threadType: threadResolution.type === ThreadType3.Group ? "group" : "user",
|
|
3991
6919
|
reason: ipcResult.reason
|
|
3992
6920
|
},
|
|
3993
6921
|
command
|
|
@@ -4020,40 +6948,52 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
4020
6948
|
}
|
|
4021
6949
|
)
|
|
4022
6950
|
);
|
|
4023
|
-
msg.command("recent <threadId>").option("-g, --group", "List recent messages for group thread").option("-n, --count <count>", "Number of messages (
|
|
6951
|
+
msg.command("recent <threadId>").option("-g, --group", "List recent messages for group thread").option("-n, --count <count>", "Number of messages", "20").option("--source <source>", "Message source: live, db, or auto", "live").option("-j, --json", "JSON output").description("List recent messages (group uses direct history API when available)").action(
|
|
4024
6952
|
wrapAction(
|
|
4025
6953
|
async (threadId, opts, command) => {
|
|
4026
|
-
const { api } = await requireApi(command);
|
|
6954
|
+
const { api, profile } = await requireApi(command);
|
|
4027
6955
|
const parsedCount = Number(opts.count);
|
|
4028
6956
|
const count = Number.isFinite(parsedCount) ? Math.min(Math.max(Math.trunc(parsedCount), 1), 200) : 20;
|
|
4029
|
-
const threadType = opts.group ?
|
|
4030
|
-
const
|
|
4031
|
-
|
|
6957
|
+
const threadType = opts.group ? ThreadType3.Group : ThreadType3.User;
|
|
6958
|
+
const source = (opts.source ?? "live").trim().toLowerCase();
|
|
6959
|
+
if (!["live", "db", "auto"].includes(source)) {
|
|
6960
|
+
throw new Error("--source must be one of: live, db, auto");
|
|
6961
|
+
}
|
|
6962
|
+
let rows = source === "db" || source === "auto" ? await listRecentMessages({
|
|
6963
|
+
profile,
|
|
4032
6964
|
threadId,
|
|
6965
|
+
threadType: opts.group ? "group" : "user",
|
|
4033
6966
|
count
|
|
4034
|
-
);
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
ts: message.data.ts,
|
|
4043
|
-
msgType: message.data.msgType,
|
|
4044
|
-
undo: {
|
|
6967
|
+
}) : [];
|
|
6968
|
+
if (source === "live" || source === "auto" && rows.length === 0) {
|
|
6969
|
+
const messages = opts.group ? await fetchRecentGroupMessagesViaApi(api, threadId, count) : await fetchRecentUserMessagesViaListener(
|
|
6970
|
+
api,
|
|
6971
|
+
threadId,
|
|
6972
|
+
count
|
|
6973
|
+
);
|
|
6974
|
+
rows = messages.map((message) => ({
|
|
4045
6975
|
msgId: message.data.msgId,
|
|
4046
6976
|
cliMsgId: message.data.cliMsgId,
|
|
4047
6977
|
threadId: message.threadId || threadId,
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
6978
|
+
threadType: message.type === ThreadType3.Group ? "group" : "user",
|
|
6979
|
+
senderId: message.data.uidFrom,
|
|
6980
|
+
senderName: message.data.dName ?? "",
|
|
6981
|
+
ts: message.data.ts,
|
|
6982
|
+
msgType: message.data.msgType,
|
|
6983
|
+
undo: {
|
|
6984
|
+
msgId: message.data.msgId,
|
|
6985
|
+
cliMsgId: message.data.cliMsgId,
|
|
6986
|
+
threadId: message.threadId || threadId,
|
|
6987
|
+
group: message.type === ThreadType3.Group
|
|
6988
|
+
},
|
|
6989
|
+
content: typeof message.data.content === "string" ? message.data.content : JSON.stringify(message.data.content)
|
|
6990
|
+
}));
|
|
6991
|
+
}
|
|
4052
6992
|
if (opts.json) {
|
|
4053
6993
|
output(
|
|
4054
6994
|
{
|
|
4055
6995
|
threadId,
|
|
4056
|
-
threadType: threadType ===
|
|
6996
|
+
threadType: threadType === ThreadType3.Group ? "group" : "user",
|
|
4057
6997
|
count: rows.length,
|
|
4058
6998
|
messages: rows
|
|
4059
6999
|
},
|
|
@@ -4073,7 +7013,7 @@ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").de
|
|
|
4073
7013
|
output(
|
|
4074
7014
|
{
|
|
4075
7015
|
threadId,
|
|
4076
|
-
threadType: type ===
|
|
7016
|
+
threadType: type === ThreadType3.Group ? "group" : "user",
|
|
4077
7017
|
pinned: true,
|
|
4078
7018
|
response
|
|
4079
7019
|
},
|
|
@@ -4089,7 +7029,7 @@ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation"
|
|
|
4089
7029
|
output(
|
|
4090
7030
|
{
|
|
4091
7031
|
threadId,
|
|
4092
|
-
threadType: type ===
|
|
7032
|
+
threadType: type === ThreadType3.Group ? "group" : "user",
|
|
4093
7033
|
pinned: false,
|
|
4094
7034
|
response
|
|
4095
7035
|
},
|
|
@@ -4678,7 +7618,7 @@ me.command("last-online <userId>").description("Get last online of a user").acti
|
|
|
4678
7618
|
output(await api.lastOnline(userId), false);
|
|
4679
7619
|
})
|
|
4680
7620
|
);
|
|
4681
|
-
program.command("listen").description("Listen for real-time incoming messages").option("-e, --echo", "Echo incoming text message").option("-p, --prefix <prefix>", "Only process text starting with prefix").option("-w, --webhook <url>", "POST message payload to webhook").option("-r, --raw", "Output JSON line payload").option("-k, --keep-alive", "Auto restart listener on disconnect").option(
|
|
7621
|
+
program.command("listen").description("Listen for real-time incoming messages").option("-e, --echo", "Echo incoming text message").option("-p, --prefix <prefix>", "Only process text starting with prefix").option("-w, --webhook <url>", "POST message payload to webhook").option("-r, --raw", "Output JSON line payload").option("--db", "Force DB persistence for this listener session").option("--no-db", "Disable DB persistence for this listener session").option("-k, --keep-alive", "Auto restart listener on disconnect").option(
|
|
4682
7622
|
"--supervised",
|
|
4683
7623
|
"Supervisor mode (disable internal retry ownership; emit lifecycle events in --raw)"
|
|
4684
7624
|
).option(
|
|
@@ -4719,6 +7659,8 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
4719
7659
|
process.env.OPENZCA_LISTEN_DOWNLOAD_QUOTE_MEDIA
|
|
4720
7660
|
);
|
|
4721
7661
|
const sessionId = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 10)}`;
|
|
7662
|
+
const selfId = api.getOwnId();
|
|
7663
|
+
const dbWriteEnabled = await shouldWriteToDb(profile, getDbWriteOverride(opts));
|
|
4722
7664
|
const emitLifecycle = (event, fields) => {
|
|
4723
7665
|
if (!lifecycleEventsEnabled) return;
|
|
4724
7666
|
console.log(
|
|
@@ -4927,7 +7869,7 @@ ${replyMediaText}` : replyMediaText;
|
|
|
4927
7869
|
processedText = processedText.trim() ? `${processedText}
|
|
4928
7870
|
${replyContextText}` : replyContextText;
|
|
4929
7871
|
}
|
|
4930
|
-
const chatType = message.type ===
|
|
7872
|
+
const chatType = message.type === ThreadType3.Group ? "group" : "user";
|
|
4931
7873
|
const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
|
|
4932
7874
|
const senderDisplayNameRaw = getStringCandidate(messageData, [
|
|
4933
7875
|
"dName",
|
|
@@ -4936,7 +7878,7 @@ ${replyContextText}` : replyContextText;
|
|
|
4936
7878
|
"displayName"
|
|
4937
7879
|
]);
|
|
4938
7880
|
const senderDisplayName = senderDisplayNameRaw || void 0;
|
|
4939
|
-
const senderNameForMetadata = message.type ===
|
|
7881
|
+
const senderNameForMetadata = message.type === ThreadType3.Group ? senderDisplayName : void 0;
|
|
4940
7882
|
const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
|
|
4941
7883
|
const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
|
|
4942
7884
|
const mentions = extractInboundMentions({
|
|
@@ -4946,6 +7888,7 @@ ${replyContextText}` : replyContextText;
|
|
|
4946
7888
|
});
|
|
4947
7889
|
const mentionIds = mentions.map((item) => item.uid);
|
|
4948
7890
|
const timestamp = toEpochSeconds(message.data.ts);
|
|
7891
|
+
const timestampMs = toEpochMs(message.data.ts);
|
|
4949
7892
|
const payload = {
|
|
4950
7893
|
threadId: message.threadId,
|
|
4951
7894
|
targetId: message.threadId,
|
|
@@ -4973,7 +7916,7 @@ ${replyContextText}` : replyContextText;
|
|
|
4973
7916
|
mentions: mentions.length > 0 ? mentions : void 0,
|
|
4974
7917
|
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
4975
7918
|
metadata: {
|
|
4976
|
-
isGroup: message.type ===
|
|
7919
|
+
isGroup: message.type === ThreadType3.Group,
|
|
4977
7920
|
chatType,
|
|
4978
7921
|
threadId: message.threadId,
|
|
4979
7922
|
targetId: message.threadId,
|
|
@@ -5011,6 +7954,52 @@ ${replyContextText}` : replyContextText;
|
|
|
5011
7954
|
toId,
|
|
5012
7955
|
ts: message.data.ts
|
|
5013
7956
|
};
|
|
7957
|
+
if (dbWriteEnabled) {
|
|
7958
|
+
const mediaForDb = mediaEntries.map((entry) => ({
|
|
7959
|
+
mediaKind: mediaKind ?? void 0,
|
|
7960
|
+
mediaUrl: entry.mediaUrl,
|
|
7961
|
+
mediaPath: entry.mediaPath,
|
|
7962
|
+
mediaType: entry.mediaType,
|
|
7963
|
+
rawJson: JSON.stringify(entry)
|
|
7964
|
+
}));
|
|
7965
|
+
const mentionsForDb = mentions.map((mention) => ({
|
|
7966
|
+
uid: mention.uid,
|
|
7967
|
+
pos: mention.pos,
|
|
7968
|
+
len: mention.len,
|
|
7969
|
+
type: mention.type,
|
|
7970
|
+
rawJson: JSON.stringify(mention)
|
|
7971
|
+
}));
|
|
7972
|
+
scheduleDbWrite(profile, command, "listen.db.persist_error", async () => {
|
|
7973
|
+
await persistMessage(
|
|
7974
|
+
normalizeInboundListenRecord({
|
|
7975
|
+
profile,
|
|
7976
|
+
threadType: chatType,
|
|
7977
|
+
rawThreadId: message.threadId,
|
|
7978
|
+
senderId,
|
|
7979
|
+
senderName: senderDisplayName,
|
|
7980
|
+
toId,
|
|
7981
|
+
selfId,
|
|
7982
|
+
title: threadName,
|
|
7983
|
+
msgId: message.data.msgId,
|
|
7984
|
+
cliMsgId: message.data.cliMsgId,
|
|
7985
|
+
actionId: getStringCandidate(messageData, ["actionId"]),
|
|
7986
|
+
timestampMs,
|
|
7987
|
+
msgType: msgType || void 0,
|
|
7988
|
+
contentText: processedText || rawText || void 0,
|
|
7989
|
+
contentJson: rawContent && typeof rawContent === "object" ? JSON.stringify(rawContent) : void 0,
|
|
7990
|
+
quoteMsgId: quote?.globalMsgId ? String(quote.globalMsgId) : void 0,
|
|
7991
|
+
quoteCliMsgId: quote?.cliMsgId ? String(quote.cliMsgId) : void 0,
|
|
7992
|
+
quoteOwnerId: quote?.ownerId ? String(quote.ownerId) : void 0,
|
|
7993
|
+
quoteText: quote?.msg,
|
|
7994
|
+
media: mediaForDb,
|
|
7995
|
+
mentions: mentionsForDb,
|
|
7996
|
+
rawMessage: message.data,
|
|
7997
|
+
rawPayload: payload,
|
|
7998
|
+
source: "listen"
|
|
7999
|
+
})
|
|
8000
|
+
);
|
|
8001
|
+
});
|
|
8002
|
+
}
|
|
5014
8003
|
if (opts.raw) {
|
|
5015
8004
|
console.log(JSON.stringify(payload));
|
|
5016
8005
|
} else {
|
|
@@ -5191,4 +8180,4 @@ ${replyContextText}` : replyContextText;
|
|
|
5191
8180
|
}
|
|
5192
8181
|
)
|
|
5193
8182
|
);
|
|
5194
|
-
program.parseAsync(process.argv);
|
|
8183
|
+
program.parseAsync(normalizeCommandAliases(process.argv));
|