openzca 0.1.49 → 0.1.51

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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { createRequire } from "module";
4
+ import { createRequire as createRequire2 } from "module";
5
5
  import { spawn as spawn2 } from "child_process";
6
6
  import fsSync from "fs";
7
7
  import fs6 from "fs/promises";
@@ -16,7 +16,7 @@ import {
16
16
  Gender,
17
17
  Reactions,
18
18
  ReviewPendingMemberRequestStatus,
19
- ThreadType as ThreadType2
19
+ ThreadType as ThreadType3
20
20
  } from "zca-js";
21
21
 
22
22
  // src/lib/store.ts
@@ -214,13 +214,273 @@ async function clearCache(profileName) {
214
214
  // src/lib/db.ts
215
215
  import crypto from "crypto";
216
216
  import fs2 from "fs/promises";
217
+ import { createRequire } from "module";
217
218
  import path2 from "path";
218
- import { open } from "sqlite";
219
- import sqlite3 from "sqlite3";
219
+ import { Worker } from "worker_threads";
220
+ var require2 = createRequire(import.meta.url);
221
+ function buildDbError(error) {
222
+ const built = new Error(error.message);
223
+ built.name = error.name || "Error";
224
+ built.stack = error.stack ?? built.stack;
225
+ built.code = error.code;
226
+ return built;
227
+ }
228
+ function resolveWorkerSpec() {
229
+ const currentUrl = new URL(import.meta.url);
230
+ if (currentUrl.pathname.endsWith("/src/lib/db.ts")) {
231
+ return {
232
+ url: new URL("./db-worker.ts", currentUrl),
233
+ execArgv: ["--import", require2.resolve("tsx")]
234
+ };
235
+ }
236
+ return {
237
+ url: new URL("./db-worker.js", currentUrl)
238
+ };
239
+ }
240
+ var Database = class _Database {
241
+ #worker;
242
+ #closed = false;
243
+ #closing = false;
244
+ #released = false;
245
+ #nextId = 1;
246
+ #activeRequests = 0;
247
+ #idleTimer;
248
+ #closePromise;
249
+ #pending = /* @__PURE__ */ new Map();
250
+ #ready;
251
+ #exited;
252
+ #releaseConnection;
253
+ constructor(worker, releaseConnection) {
254
+ this.#worker = worker;
255
+ this.#releaseConnection = releaseConnection;
256
+ let resolveReady;
257
+ let rejectReady;
258
+ this.#ready = new Promise((resolve, reject) => {
259
+ resolveReady = resolve;
260
+ rejectReady = reject;
261
+ });
262
+ let resolveExited;
263
+ this.#exited = new Promise((resolve) => {
264
+ resolveExited = resolve;
265
+ });
266
+ const rejectPending = (error) => {
267
+ for (const { reject } of this.#pending.values()) {
268
+ reject(error);
269
+ }
270
+ this.#pending.clear();
271
+ };
272
+ worker.on("message", (message) => {
273
+ if (message.type === "ready") {
274
+ resolveReady();
275
+ return;
276
+ }
277
+ if (message.type === "fatal") {
278
+ const error = buildDbError(message.error);
279
+ rejectReady(error);
280
+ rejectPending(error);
281
+ return;
282
+ }
283
+ const pending = this.#pending.get(message.id);
284
+ if (!pending) {
285
+ return;
286
+ }
287
+ this.#pending.delete(message.id);
288
+ if (message.type === "error") {
289
+ pending.reject(buildDbError(message.error));
290
+ return;
291
+ }
292
+ pending.resolve(message.result);
293
+ });
294
+ worker.once("error", (error) => {
295
+ rejectReady(error);
296
+ rejectPending(error instanceof Error ? error : new Error(String(error)));
297
+ });
298
+ worker.once("exit", (code) => {
299
+ this.#closed = true;
300
+ this.#release();
301
+ resolveExited();
302
+ const error = new Error(
303
+ code === 0 ? "DB worker exited" : `DB worker exited with code ${code}`
304
+ );
305
+ if (code !== 0) {
306
+ rejectReady(error);
307
+ }
308
+ if (this.#pending.size > 0) {
309
+ rejectPending(error);
310
+ }
311
+ });
312
+ }
313
+ static async open(filename, onClosed) {
314
+ const { url, execArgv } = resolveWorkerSpec();
315
+ const worker = new Worker(url, {
316
+ execArgv,
317
+ workerData: { filename }
318
+ });
319
+ worker.unref();
320
+ const db = new _Database(worker, onClosed);
321
+ await db.#ready;
322
+ return db;
323
+ }
324
+ get isClosing() {
325
+ return this.#closing || this.#closed;
326
+ }
327
+ #release() {
328
+ if (this.#released) {
329
+ return;
330
+ }
331
+ this.#released = true;
332
+ this.#releaseConnection();
333
+ }
334
+ #clearIdleClose() {
335
+ if (this.#idleTimer) {
336
+ clearTimeout(this.#idleTimer);
337
+ this.#idleTimer = void 0;
338
+ }
339
+ }
340
+ #scheduleIdleClose() {
341
+ this.#clearIdleClose();
342
+ this.#idleTimer = setTimeout(() => {
343
+ if (this.#activeRequests === 0 && !this.#closed) {
344
+ void this.close().catch(() => {
345
+ });
346
+ }
347
+ }, 100);
348
+ this.#idleTimer.unref();
349
+ }
350
+ async #request(type, payload) {
351
+ if (this.#closed) {
352
+ throw new Error("DB worker is closed");
353
+ }
354
+ if (this.#closing && type !== "close") {
355
+ throw new Error("DB worker is closing");
356
+ }
357
+ this.#clearIdleClose();
358
+ this.#activeRequests += 1;
359
+ await this.#ready;
360
+ const id = this.#nextId;
361
+ this.#nextId += 1;
362
+ const request = payload === void 0 ? { id, type } : { id, type, payload };
363
+ const result = new Promise((resolve, reject) => {
364
+ this.#pending.set(id, { resolve, reject });
365
+ });
366
+ try {
367
+ this.#worker.postMessage(request);
368
+ } catch (error) {
369
+ this.#pending.delete(id);
370
+ throw error;
371
+ }
372
+ try {
373
+ return await result;
374
+ } finally {
375
+ this.#activeRequests -= 1;
376
+ if (this.#activeRequests === 0 && !this.#closed && !this.#closing) {
377
+ this.#scheduleIdleClose();
378
+ }
379
+ }
380
+ }
381
+ async exec(sql) {
382
+ await this.#request("exec", { sql });
383
+ }
384
+ async run(sql, params = []) {
385
+ return await this.#request("run", { sql, params });
386
+ }
387
+ async get(sql, params = []) {
388
+ const result = await this.#request("get", { sql, params });
389
+ return result ?? void 0;
390
+ }
391
+ async all(sql, params = []) {
392
+ return await this.#request("all", { sql, params });
393
+ }
394
+ async batch(commands, transactional = false) {
395
+ await this.#request("batch", { commands, transactional });
396
+ }
397
+ async close() {
398
+ if (this.#closed) {
399
+ return;
400
+ }
401
+ if (this.#closePromise) {
402
+ return this.#closePromise;
403
+ }
404
+ this.#closePromise = (async () => {
405
+ this.#closing = true;
406
+ this.#release();
407
+ this.#clearIdleClose();
408
+ await this.#request("close");
409
+ this.#closed = true;
410
+ await this.#exited;
411
+ })();
412
+ return this.#closePromise;
413
+ }
414
+ };
220
415
  var DB_CONFIG_FILE = "db.json";
221
416
  var DB_FILENAME = "messages.sqlite";
222
417
  var connections = /* @__PURE__ */ new Map();
223
418
  var writeQueues = /* @__PURE__ */ new Map();
419
+ var UPSERT_THREAD_SQL = `
420
+ INSERT INTO threads (
421
+ profile, scope_thread_id, raw_thread_id, thread_type, peer_id, title,
422
+ is_pinned, is_hidden, is_archived, raw_json, created_at, updated_at
423
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
424
+ ON CONFLICT(profile, scope_thread_id) DO UPDATE SET
425
+ raw_thread_id = excluded.raw_thread_id,
426
+ thread_type = excluded.thread_type,
427
+ peer_id = COALESCE(excluded.peer_id, threads.peer_id),
428
+ title = COALESCE(excluded.title, threads.title),
429
+ is_pinned = excluded.is_pinned,
430
+ is_hidden = excluded.is_hidden,
431
+ is_archived = excluded.is_archived,
432
+ raw_json = COALESCE(excluded.raw_json, threads.raw_json),
433
+ updated_at = excluded.updated_at
434
+ `;
435
+ var INSERT_THREAD_MEMBER_SQL = `
436
+ INSERT INTO thread_members (
437
+ profile, scope_thread_id, user_id, display_name, zalo_name, avatar,
438
+ account_status, member_type, raw_json, snapshot_at_ms, created_at, updated_at
439
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
440
+ `;
441
+ var UPSERT_MESSAGE_SQL = `
442
+ INSERT INTO messages (
443
+ profile, message_uid, scope_thread_id, raw_thread_id, thread_type,
444
+ msg_id, cli_msg_id, action_id, sender_id, sender_name, to_id,
445
+ timestamp_ms, msg_type, content_text, content_json,
446
+ quote_msg_id, quote_cli_msg_id, quote_owner_id, quote_text,
447
+ source, raw_message_json, raw_payload_json, created_at, updated_at
448
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
449
+ ON CONFLICT(profile, message_uid) DO UPDATE SET
450
+ scope_thread_id = excluded.scope_thread_id,
451
+ raw_thread_id = excluded.raw_thread_id,
452
+ thread_type = excluded.thread_type,
453
+ msg_id = COALESCE(excluded.msg_id, messages.msg_id),
454
+ cli_msg_id = COALESCE(excluded.cli_msg_id, messages.cli_msg_id),
455
+ action_id = COALESCE(excluded.action_id, messages.action_id),
456
+ sender_id = COALESCE(excluded.sender_id, messages.sender_id),
457
+ sender_name = COALESCE(excluded.sender_name, messages.sender_name),
458
+ to_id = COALESCE(excluded.to_id, messages.to_id),
459
+ timestamp_ms = excluded.timestamp_ms,
460
+ msg_type = COALESCE(excluded.msg_type, messages.msg_type),
461
+ content_text = COALESCE(excluded.content_text, messages.content_text),
462
+ content_json = COALESCE(excluded.content_json, messages.content_json),
463
+ quote_msg_id = COALESCE(excluded.quote_msg_id, messages.quote_msg_id),
464
+ quote_cli_msg_id = COALESCE(excluded.quote_cli_msg_id, messages.quote_cli_msg_id),
465
+ quote_owner_id = COALESCE(excluded.quote_owner_id, messages.quote_owner_id),
466
+ quote_text = COALESCE(excluded.quote_text, messages.quote_text),
467
+ source = excluded.source,
468
+ raw_message_json = COALESCE(excluded.raw_message_json, messages.raw_message_json),
469
+ raw_payload_json = COALESCE(excluded.raw_payload_json, messages.raw_payload_json),
470
+ updated_at = excluded.updated_at
471
+ `;
472
+ var INSERT_MESSAGE_MEDIA_SQL = `
473
+ INSERT INTO message_media (
474
+ profile, message_uid, item_index, media_kind, media_url,
475
+ media_path, media_type, raw_json, created_at, updated_at
476
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
477
+ `;
478
+ var INSERT_MESSAGE_MENTION_SQL = `
479
+ INSERT INTO message_mentions (
480
+ profile, message_uid, item_index, target_user_id, pos, len,
481
+ mention_type, raw_json, created_at, updated_at
482
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
483
+ `;
224
484
  function nowIso2() {
225
485
  return (/* @__PURE__ */ new Date()).toISOString();
226
486
  }
@@ -326,165 +586,21 @@ async function resolveDbPath(profile) {
326
586
  }
327
587
  return path2.isAbsolute(configured) ? configured : path2.resolve(getProfileDir(profile), configured);
328
588
  }
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
589
  async function openDb(profile) {
475
590
  const filename = await resolveDbPath(profile);
476
591
  await fs2.mkdir(path2.dirname(filename), { recursive: true });
477
- const db = await open({
478
- filename,
479
- driver: sqlite3.Database
592
+ return Database.open(filename, () => {
593
+ connections.delete(profile);
480
594
  });
481
- await migrateDb(db);
482
- return db;
483
595
  }
484
596
  async function getDb(profile) {
485
597
  const existing = connections.get(profile);
486
598
  if (existing) {
487
- return existing;
599
+ const db = await existing;
600
+ if (!db.isClosing) {
601
+ return db;
602
+ }
603
+ connections.delete(profile);
488
604
  }
489
605
  const created = openDb(profile).catch((error) => {
490
606
  connections.delete(profile);
@@ -596,77 +712,48 @@ function enqueueDbWrite(profile, task) {
596
712
  async function persistThread(record) {
597
713
  const db = await getDb(record.profile);
598
714
  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
- );
715
+ await db.run(UPSERT_THREAD_SQL, [
716
+ record.profile,
717
+ record.scopeThreadId,
718
+ record.rawThreadId,
719
+ record.threadType,
720
+ record.peerId ?? null,
721
+ record.title ?? null,
722
+ record.isPinned ? 1 : 0,
723
+ record.isHidden ? 1 : 0,
724
+ record.isArchived ? 1 : 0,
725
+ record.rawJson ?? null,
726
+ now,
727
+ now
728
+ ]);
631
729
  }
632
730
  async function replaceThreadMembers(profile, scopeThreadId, members) {
633
731
  const db = await getDb(profile);
634
732
  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
- }
733
+ const commands = [
734
+ {
735
+ sql: `DELETE FROM thread_members WHERE profile = ? AND scope_thread_id = ?`,
736
+ params: [profile, scopeThreadId]
737
+ },
738
+ ...members.map((member) => ({
739
+ sql: INSERT_THREAD_MEMBER_SQL,
740
+ params: [
741
+ member.profile,
742
+ member.scopeThreadId,
743
+ member.userId,
744
+ member.displayName ?? null,
745
+ member.zaloName ?? null,
746
+ member.avatar ?? null,
747
+ member.accountStatus ?? null,
748
+ member.memberType ?? null,
749
+ member.rawJson ?? null,
750
+ member.snapshotAtMs,
751
+ now,
752
+ now
753
+ ]
754
+ }))
755
+ ];
756
+ await db.batch(commands, true);
670
757
  }
671
758
  async function persistFriend(record) {
672
759
  const db = await getDb(record.profile);
@@ -726,49 +813,27 @@ async function persistMessage(record) {
726
813
  const db = await getDb(record.profile);
727
814
  const now = nowIso2();
728
815
  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
- [
816
+ const commands = [
817
+ {
818
+ sql: UPSERT_THREAD_SQL,
819
+ params: [
820
+ record.profile,
821
+ record.scopeThreadId,
822
+ record.rawThreadId,
823
+ record.threadType,
824
+ record.peerId ?? null,
825
+ record.title ?? null,
826
+ 0,
827
+ 0,
828
+ 0,
829
+ null,
830
+ now,
831
+ now
832
+ ]
833
+ },
834
+ {
835
+ sql: UPSERT_MESSAGE_SQL,
836
+ params: [
772
837
  record.profile,
773
838
  messageUid,
774
839
  record.scopeThreadId,
@@ -794,64 +859,47 @@ async function persistMessage(record) {
794
859
  now,
795
860
  now
796
861
  ]
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
- }
862
+ },
863
+ {
864
+ sql: `DELETE FROM message_media WHERE profile = ? AND message_uid = ?`,
865
+ params: [record.profile, messageUid]
866
+ },
867
+ {
868
+ sql: `DELETE FROM message_mentions WHERE profile = ? AND message_uid = ?`,
869
+ params: [record.profile, messageUid]
870
+ },
871
+ ...(record.media ?? []).map((media, index) => ({
872
+ sql: INSERT_MESSAGE_MEDIA_SQL,
873
+ params: [
874
+ record.profile,
875
+ messageUid,
876
+ index,
877
+ media.mediaKind ?? null,
878
+ media.mediaUrl ?? null,
879
+ media.mediaPath ?? null,
880
+ media.mediaType ?? null,
881
+ media.rawJson ?? null,
882
+ now,
883
+ now
884
+ ]
885
+ })),
886
+ ...(record.mentions ?? []).map((mention, index) => ({
887
+ sql: INSERT_MESSAGE_MENTION_SQL,
888
+ params: [
889
+ record.profile,
890
+ messageUid,
891
+ index,
892
+ mention.uid,
893
+ mention.pos ?? null,
894
+ mention.len ?? null,
895
+ mention.type ?? null,
896
+ mention.rawJson ?? null,
897
+ now,
898
+ now
899
+ ]
900
+ }))
901
+ ];
902
+ await db.batch(commands, true);
855
903
  }
856
904
  async function setSyncState(params) {
857
905
  const db = await getDb(params.profile);
@@ -2494,9 +2542,234 @@ async function sendNativeVideo(params) {
2494
2542
  }
2495
2543
  }
2496
2544
 
2545
+ // src/lib/reply.ts
2546
+ import { ThreadType as ThreadType2 } from "zca-js";
2547
+ function prepareReplyMessage(value, params) {
2548
+ const sourceRecord = asReplyMessageRecord(value);
2549
+ const metadata = asOptionalReplyMessageRecord(sourceRecord.metadata);
2550
+ const rawMessageRecord = asOptionalReplyMessageRecord(sourceRecord.rawMessage);
2551
+ const rawPayloadRecord = asOptionalReplyMessageRecord(sourceRecord.rawPayload);
2552
+ const canonicalRecord = rawMessageRecord ?? sourceRecord;
2553
+ const content = parseReplyMessageContent(
2554
+ canonicalRecord.content ?? sourceRecord.content,
2555
+ isLikelyOpenzcaListenPayload(sourceRecord) && !rawMessageRecord
2556
+ );
2557
+ const msgType = requireStringLike(
2558
+ [canonicalRecord.msgType, sourceRecord.msgType, metadata?.msgType],
2559
+ "reply message msgType"
2560
+ );
2561
+ const uidFrom = requireStringLike(
2562
+ [
2563
+ canonicalRecord.uidFrom,
2564
+ sourceRecord.uidFrom,
2565
+ sourceRecord.senderId,
2566
+ sourceRecord.fromId,
2567
+ metadata?.senderId,
2568
+ metadata?.fromId
2569
+ ],
2570
+ "reply message uidFrom"
2571
+ );
2572
+ const msgId = requireStringLike(
2573
+ [canonicalRecord.msgId, sourceRecord.msgId, rawPayloadRecord?.msgId],
2574
+ "reply message msgId"
2575
+ );
2576
+ const cliMsgId = requireStringLike(
2577
+ [canonicalRecord.cliMsgId, sourceRecord.cliMsgId, rawPayloadRecord?.cliMsgId],
2578
+ "reply message cliMsgId"
2579
+ );
2580
+ const ts = requireTsString(
2581
+ [canonicalRecord.ts, sourceRecord.ts, maybeTimestampSecondsToMsString(sourceRecord.timestamp)],
2582
+ "reply message ts"
2583
+ );
2584
+ const ttl = parseReplyMessageTtl(canonicalRecord.ttl ?? sourceRecord.ttl);
2585
+ const propertyExt = parseReplyMessagePropertyExt(canonicalRecord.propertyExt);
2586
+ return {
2587
+ quote: {
2588
+ content,
2589
+ msgType,
2590
+ propertyExt,
2591
+ uidFrom,
2592
+ msgId,
2593
+ cliMsgId,
2594
+ ts,
2595
+ ttl
2596
+ },
2597
+ inferredThreadId: inferReplyMessageThreadId({
2598
+ sourceRecord,
2599
+ canonicalRecord,
2600
+ metadata,
2601
+ threadType: params?.threadType,
2602
+ selfId: params?.selfId
2603
+ })
2604
+ };
2605
+ }
2606
+ function prepareStoredReplyMessage(value, params) {
2607
+ const record = asReplyMessageRecord(value);
2608
+ const storedThreadType = record.threadType === "group" ? ThreadType2.Group : record.threadType === "user" ? ThreadType2.User : void 0;
2609
+ if (storedThreadType !== void 0 && storedThreadType !== params.threadType) {
2610
+ throw new Error("Reply source thread type does not match --group.");
2611
+ }
2612
+ const storedThreadId = firstString([record.threadId, record.rawThreadId]) ?? void 0;
2613
+ if (storedThreadId && storedThreadId !== params.threadId) {
2614
+ throw new Error("Reply source belongs to a different thread.");
2615
+ }
2616
+ const rawMessage = asOptionalReplyMessageRecord(record.rawMessage);
2617
+ const rawPayload = asOptionalReplyMessageRecord(record.rawPayload);
2618
+ const replyRecord = rawMessage ?? rawPayload;
2619
+ if (!replyRecord) {
2620
+ throw new Error(
2621
+ "Reply source found in DB but has no reusable raw message payload. Re-sync or capture it via listener first."
2622
+ );
2623
+ }
2624
+ return prepareReplyMessage(replyRecord, {
2625
+ threadType: params.threadType,
2626
+ selfId: params.selfId
2627
+ }).quote;
2628
+ }
2629
+ function asReplyMessageRecord(value) {
2630
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2631
+ throw new Error("Reply message must be a JSON object matching the raw message.data shape.");
2632
+ }
2633
+ return value;
2634
+ }
2635
+ function asOptionalReplyMessageRecord(value) {
2636
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2637
+ return void 0;
2638
+ }
2639
+ return value;
2640
+ }
2641
+ function parseReplyMessageContent(value, stripOpenzcaDecorations) {
2642
+ if (typeof value === "string") {
2643
+ return stripOpenzcaDecorations ? stripEnrichedReplyDecorations(value) : value;
2644
+ }
2645
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2646
+ return value;
2647
+ }
2648
+ throw new Error("Reply message content must be a string or object.");
2649
+ }
2650
+ function stripEnrichedReplyDecorations(value) {
2651
+ const lines = value.split("\n");
2652
+ while (lines.length > 0) {
2653
+ const last = lines[lines.length - 1].trim();
2654
+ if (last.startsWith("[reply context: ") || last.startsWith("[reply media attached:") || last.startsWith("[reply media attached ")) {
2655
+ lines.pop();
2656
+ continue;
2657
+ }
2658
+ break;
2659
+ }
2660
+ return lines.join("\n");
2661
+ }
2662
+ function parseReplyMessagePropertyExt(value) {
2663
+ if (value === void 0) {
2664
+ return void 0;
2665
+ }
2666
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2667
+ throw new Error("Reply message propertyExt must be an object when provided.");
2668
+ }
2669
+ return value;
2670
+ }
2671
+ function parseReplyMessageTtl(value) {
2672
+ if (value === void 0 || value === null || value === "") {
2673
+ return 0;
2674
+ }
2675
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
2676
+ if (!Number.isFinite(parsed)) {
2677
+ throw new Error("Reply message ttl must be a finite number.");
2678
+ }
2679
+ return Math.trunc(parsed);
2680
+ }
2681
+ function requireStringLike(values, label) {
2682
+ const value = firstString(values);
2683
+ if (!value) {
2684
+ throw new Error(`Missing ${label}.`);
2685
+ }
2686
+ return value;
2687
+ }
2688
+ function requireTsString(values, label) {
2689
+ for (const value of values) {
2690
+ if (typeof value === "string" && value.trim()) {
2691
+ return value.trim();
2692
+ }
2693
+ if (typeof value === "number" && Number.isFinite(value)) {
2694
+ return String(Math.trunc(value));
2695
+ }
2696
+ }
2697
+ throw new Error(`Missing ${label}.`);
2698
+ }
2699
+ function firstString(values) {
2700
+ for (const value of values) {
2701
+ if (typeof value === "string" && value.trim()) {
2702
+ return value.trim();
2703
+ }
2704
+ if (typeof value === "number" && Number.isFinite(value)) {
2705
+ return String(Math.trunc(value));
2706
+ }
2707
+ }
2708
+ return void 0;
2709
+ }
2710
+ function maybeTimestampSecondsToMsString(value) {
2711
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2712
+ return void 0;
2713
+ }
2714
+ return String(Math.trunc(value * 1e3));
2715
+ }
2716
+ function isLikelyOpenzcaListenPayload(record) {
2717
+ return typeof record.threadId === "string" && (typeof record.senderId === "string" || typeof record.chatType === "string" || typeof record.metadata === "object");
2718
+ }
2719
+ function inferReplyMessageThreadId(params) {
2720
+ const directThreadId = firstString([
2721
+ params.sourceRecord.threadId,
2722
+ params.sourceRecord.targetId,
2723
+ params.sourceRecord.conversationId,
2724
+ params.metadata?.threadId,
2725
+ params.metadata?.targetId
2726
+ ]);
2727
+ if (directThreadId) {
2728
+ return directThreadId;
2729
+ }
2730
+ if (params.threadType === void 0) {
2731
+ return void 0;
2732
+ }
2733
+ const idTo = firstString([
2734
+ params.canonicalRecord.idTo,
2735
+ params.sourceRecord.idTo,
2736
+ params.sourceRecord.toId,
2737
+ params.metadata?.toId
2738
+ ]);
2739
+ if (params.threadType === ThreadType2.Group) {
2740
+ return idTo;
2741
+ }
2742
+ const uidFrom = firstString([
2743
+ params.canonicalRecord.uidFrom,
2744
+ params.sourceRecord.uidFrom,
2745
+ params.sourceRecord.senderId,
2746
+ params.sourceRecord.fromId,
2747
+ params.metadata?.senderId,
2748
+ params.metadata?.fromId
2749
+ ]);
2750
+ if (!uidFrom && !idTo) {
2751
+ return void 0;
2752
+ }
2753
+ if (params.selfId) {
2754
+ if (uidFrom && uidFrom !== params.selfId && uidFrom !== "0") {
2755
+ return uidFrom;
2756
+ }
2757
+ if (idTo && idTo !== params.selfId && idTo !== "0") {
2758
+ return idTo;
2759
+ }
2760
+ }
2761
+ if (uidFrom && uidFrom !== "0") {
2762
+ return uidFrom;
2763
+ }
2764
+ if (idTo && idTo !== "0") {
2765
+ return idTo;
2766
+ }
2767
+ return void 0;
2768
+ }
2769
+
2497
2770
  // src/cli.ts
2498
- var require2 = createRequire(import.meta.url);
2499
- var { version: PKG_VERSION } = require2("../package.json");
2771
+ var require3 = createRequire2(import.meta.url);
2772
+ var { version: PKG_VERSION } = require3("../package.json");
2500
2773
  var program = new Command();
2501
2774
  var EMOJI_REACTION_MAP = {
2502
2775
  "\u2764\uFE0F": Reactions.HEART,
@@ -2656,7 +2929,7 @@ function normalizeCommandAliases(argv) {
2656
2929
  return normalized;
2657
2930
  }
2658
2931
  function asThreadType(groupFlag) {
2659
- return groupFlag ? ThreadType2.Group : ThreadType2.User;
2932
+ return groupFlag ? ThreadType3.Group : ThreadType3.User;
2660
2933
  }
2661
2934
  function parseBooleanFromEnv(name, fallback) {
2662
2935
  const raw = process.env[name]?.trim();
@@ -2866,7 +3139,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
2866
3139
  fail(parsed.requestId, "Invalid upload payload.");
2867
3140
  return;
2868
3141
  }
2869
- const threadType = parsed.threadType === "group" ? ThreadType2.Group : ThreadType2.User;
3142
+ const threadType = parsed.threadType === "group" ? ThreadType3.Group : ThreadType3.User;
2870
3143
  const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
2871
3144
  writeDebugLine(
2872
3145
  "listen.ipc.upload.start",
@@ -3023,7 +3296,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
3023
3296
  {
3024
3297
  profile,
3025
3298
  threadId,
3026
- threadType: threadType === ThreadType2.Group ? "group" : "user",
3299
+ threadType: threadType === ThreadType3.Group ? "group" : "user",
3027
3300
  attachmentCount: attachments.length,
3028
3301
  socketPath,
3029
3302
  requestId,
@@ -3067,7 +3340,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
3067
3340
  requestId,
3068
3341
  profile,
3069
3342
  threadId,
3070
- threadType: threadType === ThreadType2.Group ? "group" : "user",
3343
+ threadType: threadType === ThreadType3.Group ? "group" : "user",
3071
3344
  attachments
3072
3345
  };
3073
3346
  socket.write(`${JSON.stringify(payload)}
@@ -3129,21 +3402,21 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
3129
3402
  }
3130
3403
  async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
3131
3404
  if (groupFlag) {
3132
- return { type: ThreadType2.Group, reason: "explicit_group_flag" };
3405
+ return { type: ThreadType3.Group, reason: "explicit_group_flag" };
3133
3406
  }
3134
3407
  const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", false);
3135
3408
  if (!autoDetectEnabled) {
3136
- return { type: ThreadType2.User, reason: "auto_detect_disabled" };
3409
+ return { type: ThreadType3.User, reason: "auto_detect_disabled" };
3137
3410
  }
3138
3411
  try {
3139
3412
  const cache = await readCache(profile);
3140
3413
  const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
3141
3414
  if (groupIds.has(threadId)) {
3142
- return { type: ThreadType2.Group, reason: "cache_group_match" };
3415
+ return { type: ThreadType3.Group, reason: "cache_group_match" };
3143
3416
  }
3144
3417
  const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
3145
3418
  if (friendIds.has(threadId)) {
3146
- return { type: ThreadType2.User, reason: "cache_friend_match" };
3419
+ return { type: ThreadType3.User, reason: "cache_friend_match" };
3147
3420
  }
3148
3421
  } catch (error) {
3149
3422
  writeDebugLine(
@@ -3158,7 +3431,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3158
3431
  }
3159
3432
  const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
3160
3433
  if (!probeEnabled) {
3161
- return { type: ThreadType2.User, reason: "probe_disabled" };
3434
+ return { type: ThreadType3.User, reason: "probe_disabled" };
3162
3435
  }
3163
3436
  const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
3164
3437
  try {
@@ -3168,7 +3441,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3168
3441
  `Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
3169
3442
  );
3170
3443
  if (groupInfo?.gridInfoMap?.[threadId]) {
3171
- return { type: ThreadType2.Group, reason: "probe_group_match" };
3444
+ return { type: ThreadType3.Group, reason: "probe_group_match" };
3172
3445
  }
3173
3446
  } catch (error) {
3174
3447
  writeDebugLine(
@@ -3181,7 +3454,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3181
3454
  command
3182
3455
  );
3183
3456
  }
3184
- return { type: ThreadType2.User, reason: "default_user" };
3457
+ return { type: ThreadType3.User, reason: "default_user" };
3185
3458
  }
3186
3459
  function parseReaction(input) {
3187
3460
  const normalized = input.trim();
@@ -3279,6 +3552,62 @@ async function shouldWriteToDb(profile, override) {
3279
3552
  }
3280
3553
  return isDbEnabled(profile);
3281
3554
  }
3555
+ async function resolveSendReplyQuote(params) {
3556
+ const replyId = params.replyId?.trim();
3557
+ const replyMessage = params.replyMessage?.trim();
3558
+ if (replyId && replyMessage) {
3559
+ throw new Error("Use either --reply-id or --reply-message, not both.");
3560
+ }
3561
+ if (!replyId && !replyMessage) {
3562
+ return void 0;
3563
+ }
3564
+ if (replyId) {
3565
+ if (!await shouldWriteToDb(params.profile)) {
3566
+ throw new Error("`--reply-id` requires the local DB. Enable DB/listen sync first.");
3567
+ }
3568
+ const row = await getMessageById({
3569
+ profile: params.profile,
3570
+ id: replyId
3571
+ });
3572
+ if (!row) {
3573
+ throw new Error(`Reply source not found in DB: ${replyId}`);
3574
+ }
3575
+ if (row.threadType === "group" !== (params.threadType === ThreadType3.Group)) {
3576
+ throw new Error("Reply source thread type does not match --group.");
3577
+ }
3578
+ if (row.threadId !== params.threadId) {
3579
+ throw new Error("Reply source belongs to a different thread.");
3580
+ }
3581
+ if (!row.rawMessage || typeof row.rawMessage !== "object") {
3582
+ if (!row.rawPayload || typeof row.rawPayload !== "object") {
3583
+ throw new Error(
3584
+ "Reply source found in DB but has no reusable raw message payload. Re-sync or capture it via listener first."
3585
+ );
3586
+ }
3587
+ }
3588
+ return prepareStoredReplyMessage(row, {
3589
+ threadId: params.threadId,
3590
+ threadType: params.threadType,
3591
+ selfId: params.api.getOwnId()
3592
+ });
3593
+ }
3594
+ let parsedReplyMessage;
3595
+ try {
3596
+ parsedReplyMessage = JSON.parse(replyMessage);
3597
+ } catch (error) {
3598
+ throw new Error(
3599
+ `Invalid JSON for --reply-message: ${error instanceof Error ? error.message : String(error)}`
3600
+ );
3601
+ }
3602
+ const preparedReply = prepareReplyMessage(parsedReplyMessage, {
3603
+ threadType: params.threadType,
3604
+ selfId: params.api.getOwnId()
3605
+ });
3606
+ if (preparedReply.inferredThreadId && preparedReply.inferredThreadId !== params.threadId) {
3607
+ throw new Error("Reply message belongs to a different thread.");
3608
+ }
3609
+ return preparedReply.quote;
3610
+ }
3282
3611
  function scheduleDbWrite(profile, command, event, task) {
3283
3612
  enqueueDbWrite(profile, async () => {
3284
3613
  try {
@@ -4259,7 +4588,7 @@ function normalizeGroupHistoryMessages(messages, fallbackThreadId) {
4259
4588
  const threadIdRaw = String(raw.idTo ?? "").trim();
4260
4589
  normalized.push({
4261
4590
  threadId: threadIdRaw || fallbackThreadId,
4262
- type: ThreadType2.Group,
4591
+ type: ThreadType3.Group,
4263
4592
  data: {
4264
4593
  actionId: typeof raw.actionId === "string" && raw.actionId.trim() ? raw.actionId : void 0,
4265
4594
  msgId: String(raw.msgId ?? ""),
@@ -4364,7 +4693,7 @@ async function crawlGroupHistoryViaListener(api, options) {
4364
4693
  requestedCursors.add(cursor);
4365
4694
  }
4366
4695
  pagesRequested += 1;
4367
- api.listener.requestOldMessages(ThreadType2.Group, cursor || null);
4696
+ api.listener.requestOldMessages(ThreadType3.Group, cursor || null);
4368
4697
  return true;
4369
4698
  };
4370
4699
  const armIdleTimer = () => {
@@ -4419,7 +4748,7 @@ async function crawlGroupHistoryViaListener(api, options) {
4419
4748
  }
4420
4749
  };
4421
4750
  const onOldMessages = (messages, type) => {
4422
- if (type !== ThreadType2.Group) return;
4751
+ if (type !== ThreadType3.Group) return;
4423
4752
  armIdleTimer();
4424
4753
  const typedMessages = messages;
4425
4754
  processing = processing.then(async () => {
@@ -4508,7 +4837,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
4508
4837
  requestedCursors.add(cursor);
4509
4838
  }
4510
4839
  pagesRequested += 1;
4511
- api.listener.requestOldMessages(ThreadType2.User, cursor || null);
4840
+ api.listener.requestOldMessages(ThreadType3.User, cursor || null);
4512
4841
  return true;
4513
4842
  };
4514
4843
  const cleanup = () => {
@@ -4540,7 +4869,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
4540
4869
  }
4541
4870
  };
4542
4871
  const onOldMessages = (messages, type) => {
4543
- if (type !== ThreadType2.User) return;
4872
+ if (type !== ThreadType3.User) return;
4544
4873
  const typedMessages = messages;
4545
4874
  for (const message of typedMessages) {
4546
4875
  if (message.threadId === threadId) {
@@ -4616,7 +4945,7 @@ async function fetchRecentUserMessagesAcrossThreads(api, maxMessages) {
4616
4945
  requestedCursors.add(cursor);
4617
4946
  }
4618
4947
  pagesRequested += 1;
4619
- api.listener.requestOldMessages(ThreadType2.User, cursor || null);
4948
+ api.listener.requestOldMessages(ThreadType3.User, cursor || null);
4620
4949
  return true;
4621
4950
  };
4622
4951
  const cleanup = () => {
@@ -4648,7 +4977,7 @@ async function fetchRecentUserMessagesAcrossThreads(api, maxMessages) {
4648
4977
  }
4649
4978
  };
4650
4979
  const onOldMessages = (messages, type) => {
4651
- if (type !== ThreadType2.User) return;
4980
+ if (type !== ThreadType3.User) return;
4652
4981
  const typedMessages = messages;
4653
4982
  for (const message of typedMessages) {
4654
4983
  const key = toKey(message);
@@ -4731,7 +5060,7 @@ function toDbRecordFromRecentMessage(params) {
4731
5060
  const quote = params.message.data?.quote;
4732
5061
  return normalizeInboundListenRecord({
4733
5062
  profile: params.profile,
4734
- threadType: params.message.type === ThreadType2.Group ? "group" : "user",
5063
+ threadType: params.message.type === ThreadType3.Group ? "group" : "user",
4735
5064
  rawThreadId: params.message.threadId,
4736
5065
  senderId: params.message.data?.uidFrom,
4737
5066
  senderName: params.message.data?.dName,
@@ -6083,17 +6412,29 @@ dbSync.command("chat <chatId>").option("-n, --count <count>", "Recent messages t
6083
6412
  })
6084
6413
  );
6085
6414
  var msg = program.command("msg").description("Messaging commands");
6086
- 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(
6415
+ 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(
6087
6416
  wrapAction(async (threadId, message, opts, command) => {
6088
6417
  const { api, profile } = await requireApi(command);
6089
6418
  const threadType = asThreadType(opts.group);
6090
- const payload = await buildTextSendPayload({
6419
+ const textPayload = await buildTextSendPayload({
6091
6420
  message,
6092
6421
  raw: opts.raw,
6093
6422
  threadType,
6094
6423
  threadId,
6095
- listGroupMembers: threadType === ThreadType2.Group ? (groupId) => listGroupMentionMembers(api, groupId) : void 0
6424
+ listGroupMembers: threadType === ThreadType3.Group ? (groupId) => listGroupMentionMembers(api, groupId) : void 0
6425
+ });
6426
+ const quote = await resolveSendReplyQuote({
6427
+ profile,
6428
+ api,
6429
+ threadId,
6430
+ threadType,
6431
+ replyId: opts.replyId,
6432
+ replyMessage: opts.replyMessage
6096
6433
  });
6434
+ const payload = quote || typeof textPayload !== "string" ? {
6435
+ ...typeof textPayload === "string" ? { msg: textPayload } : textPayload,
6436
+ ...quote ? { quote } : {}
6437
+ } : textPayload;
6097
6438
  const response = await api.sendMessage(payload, threadId, threadType);
6098
6439
  output(response, false);
6099
6440
  if (await shouldWriteToDb(profile)) {
@@ -6582,8 +6923,8 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
6582
6923
  {
6583
6924
  threadId,
6584
6925
  explicitGroupFlag: Boolean(opts.group),
6585
- isGroup: threadResolution.type === ThreadType2.Group,
6586
- threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
6926
+ isGroup: threadResolution.type === ThreadType3.Group,
6927
+ threadType: threadResolution.type === ThreadType3.Group ? "group" : "user",
6587
6928
  threadTypeReason: threadResolution.reason,
6588
6929
  localFiles,
6589
6930
  urlInputs
@@ -6611,7 +6952,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
6611
6952
  "msg.upload.ipc.done",
6612
6953
  {
6613
6954
  threadId,
6614
- threadType: threadResolution.type === ThreadType2.Group ? "group" : "user"
6955
+ threadType: threadResolution.type === ThreadType3.Group ? "group" : "user"
6615
6956
  },
6616
6957
  command
6617
6958
  );
@@ -6622,7 +6963,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
6622
6963
  "msg.upload.ipc.fallback",
6623
6964
  {
6624
6965
  threadId,
6625
- threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
6966
+ threadType: threadResolution.type === ThreadType3.Group ? "group" : "user",
6626
6967
  reason: ipcResult.reason
6627
6968
  },
6628
6969
  command
@@ -6661,7 +7002,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6661
7002
  const { api, profile } = await requireApi(command);
6662
7003
  const parsedCount = Number(opts.count);
6663
7004
  const count = Number.isFinite(parsedCount) ? Math.min(Math.max(Math.trunc(parsedCount), 1), 200) : 20;
6664
- const threadType = opts.group ? ThreadType2.Group : ThreadType2.User;
7005
+ const threadType = opts.group ? ThreadType3.Group : ThreadType3.User;
6665
7006
  const source = (opts.source ?? "live").trim().toLowerCase();
6666
7007
  if (!["live", "db", "auto"].includes(source)) {
6667
7008
  throw new Error("--source must be one of: live, db, auto");
@@ -6682,7 +7023,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6682
7023
  msgId: message.data.msgId,
6683
7024
  cliMsgId: message.data.cliMsgId,
6684
7025
  threadId: message.threadId || threadId,
6685
- threadType: message.type === ThreadType2.Group ? "group" : "user",
7026
+ threadType: message.type === ThreadType3.Group ? "group" : "user",
6686
7027
  senderId: message.data.uidFrom,
6687
7028
  senderName: message.data.dName ?? "",
6688
7029
  ts: message.data.ts,
@@ -6691,7 +7032,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6691
7032
  msgId: message.data.msgId,
6692
7033
  cliMsgId: message.data.cliMsgId,
6693
7034
  threadId: message.threadId || threadId,
6694
- group: message.type === ThreadType2.Group
7035
+ group: message.type === ThreadType3.Group
6695
7036
  },
6696
7037
  content: typeof message.data.content === "string" ? message.data.content : JSON.stringify(message.data.content)
6697
7038
  }));
@@ -6700,7 +7041,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6700
7041
  output(
6701
7042
  {
6702
7043
  threadId,
6703
- threadType: threadType === ThreadType2.Group ? "group" : "user",
7044
+ threadType: threadType === ThreadType3.Group ? "group" : "user",
6704
7045
  count: rows.length,
6705
7046
  messages: rows
6706
7047
  },
@@ -6720,7 +7061,7 @@ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").de
6720
7061
  output(
6721
7062
  {
6722
7063
  threadId,
6723
- threadType: type === ThreadType2.Group ? "group" : "user",
7064
+ threadType: type === ThreadType3.Group ? "group" : "user",
6724
7065
  pinned: true,
6725
7066
  response
6726
7067
  },
@@ -6736,7 +7077,7 @@ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation"
6736
7077
  output(
6737
7078
  {
6738
7079
  threadId,
6739
- threadType: type === ThreadType2.Group ? "group" : "user",
7080
+ threadType: type === ThreadType3.Group ? "group" : "user",
6740
7081
  pinned: false,
6741
7082
  response
6742
7083
  },
@@ -7576,7 +7917,7 @@ ${replyMediaText}` : replyMediaText;
7576
7917
  processedText = processedText.trim() ? `${processedText}
7577
7918
  ${replyContextText}` : replyContextText;
7578
7919
  }
7579
- const chatType = message.type === ThreadType2.Group ? "group" : "user";
7920
+ const chatType = message.type === ThreadType3.Group ? "group" : "user";
7580
7921
  const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
7581
7922
  const senderDisplayNameRaw = getStringCandidate(messageData, [
7582
7923
  "dName",
@@ -7585,7 +7926,7 @@ ${replyContextText}` : replyContextText;
7585
7926
  "displayName"
7586
7927
  ]);
7587
7928
  const senderDisplayName = senderDisplayNameRaw || void 0;
7588
- const senderNameForMetadata = message.type === ThreadType2.Group ? senderDisplayName : void 0;
7929
+ const senderNameForMetadata = message.type === ThreadType3.Group ? senderDisplayName : void 0;
7589
7930
  const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
7590
7931
  const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
7591
7932
  const mentions = extractInboundMentions({
@@ -7623,7 +7964,7 @@ ${replyContextText}` : replyContextText;
7623
7964
  mentions: mentions.length > 0 ? mentions : void 0,
7624
7965
  mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
7625
7966
  metadata: {
7626
- isGroup: message.type === ThreadType2.Group,
7967
+ isGroup: message.type === ThreadType3.Group,
7627
7968
  chatType,
7628
7969
  threadId: message.threadId,
7629
7970
  targetId: message.threadId,