imsg-mcp 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/dist/cli.js +2 -2
- package/dist/{imessage-db-BXrBHlvx.js → imessage-db-vrzopRGT.js} +176 -71
- package/dist/imessage-db-vrzopRGT.js.map +1 -0
- package/dist/index.js +172 -191
- package/dist/index.js.map +1 -1
- package/dist/{meta-DSkZz63Q.js → meta-BrYyl7Ls.js} +2 -2
- package/dist/{meta-DSkZz63Q.js.map → meta-BrYyl7Ls.js.map} +1 -1
- package/dist/tui.js +142 -110
- package/dist/tui.js.map +1 -1
- package/dist/watchdog-Cl5PoHhW.js.map +1 -1
- package/package.json +1 -1
- package/skills/imsg-mcp/SKILL.md +7 -1
- package/dist/imessage-db-BXrBHlvx.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and follows [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
# [1.9.0](https://github.com/george43g/imsg-mcp/compare/v1.8.0...v1.9.0) (2026-07-21)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **db:** extract Apple-native voice-note transcripts, Genmoji & reply kinds ([6b9d715](https://github.com/george43g/imsg-mcp/commit/6b9d715dd6c356a337b8bb0b3ef96ff40419a73a))
|
|
12
|
+
|
|
6
13
|
# [1.8.0](https://github.com/george43g/imsg-mcp/compare/v1.7.0...v1.8.0) (2026-07-20)
|
|
7
14
|
|
|
8
15
|
|
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Full reference (CLI subcommands + MCP tools + every flag): [**docs/TOOLS.md**](d
|
|
|
108
108
|
imsg tui
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Vim-style: `j/k` move, `gg/G` jump, `Enter` drawer, `o` Quick Look an attachment, `:` jump to date, `V` visual select, `e` export, `S` send via other app, `y` copy slug, `c` compose in current thread, `N` compose to new recipient (phone / email / contact name), `q` quit.
|
|
111
|
+
Vim-style: `j/k` move, `gg/G` jump, `Enter` drawer, `i` per-thread info + attachment browser, `o` Quick Look an attachment, `:` jump to date, `V` visual select, `e` export, `S` send via other app, `y` copy slug, `c` compose in current thread, `N` compose to new recipient (phone / email / contact name), `q` quit.
|
|
112
112
|
|
|
113
113
|
Themes (`safe` / `powerline`) and a single accent color drive the whole palette. See [docs/TOOLS.md#tui-configuration](docs/TOOLS.md#tui-configuration).
|
|
114
114
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createInterface } from "node:readline";
|
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { c as checkLocalAccess, f as formatAccessReport, J as IMPLEMENTED_TYPES, K as ANALYTIC_INFO, w as installShutdownHandlers, x as registerCleanup, L as looksLikeThreadSlug } from "./shutdown-CQ9wzrxA.js";
|
|
7
|
-
import { A as APP_VERSION, t as toYaml } from "./meta-
|
|
7
|
+
import { A as APP_VERSION, t as toYaml } from "./meta-BrYyl7Ls.js";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { join, dirname } from "node:path";
|
|
10
10
|
function distRoot() {
|
|
@@ -447,7 +447,7 @@ async function runExportCommand(target, opts) {
|
|
|
447
447
|
const { homedir } = await import("node:os");
|
|
448
448
|
const { dirname: dirname2, join: join2, isAbsolute, resolve } = await import("node:path");
|
|
449
449
|
const { getContactsDbPaths, getImsgDbPath, getSlugsDbPath } = await import("./shutdown-CQ9wzrxA.js").then((n) => n.a0);
|
|
450
|
-
const { IMessageDB } = await import("./imessage-db-
|
|
450
|
+
const { IMessageDB } = await import("./imessage-db-vrzopRGT.js").then((n) => n.i);
|
|
451
451
|
const { streamExport } = await import("./exportStream-DgPVRZTZ.js");
|
|
452
452
|
const { parseUserDate } = await import("./date-parse-DJXMfq3a.js");
|
|
453
453
|
const format = normalizeFormat(opts.format ?? "md");
|
|
@@ -767,6 +767,47 @@ function scoreCandidate(text) {
|
|
|
767
767
|
return score;
|
|
768
768
|
}
|
|
769
769
|
const STRUCTURED_BOOST = 500;
|
|
770
|
+
const AUDIO_TRANSCRIPTION_MARKER = Buffer.from("IMAudioTranscription", "ascii");
|
|
771
|
+
const AUDIO_VALUE_FRAMING = Buffer.from([134, 146, 132, 150, 150]);
|
|
772
|
+
function extractAudioTranscription(blob) {
|
|
773
|
+
if (!blob) return void 0;
|
|
774
|
+
const markerAt = blob.indexOf(AUDIO_TRANSCRIPTION_MARKER);
|
|
775
|
+
if (markerAt === -1) return void 0;
|
|
776
|
+
let pos = markerAt + AUDIO_TRANSCRIPTION_MARKER.length;
|
|
777
|
+
if (blob.subarray(pos, pos + AUDIO_VALUE_FRAMING.length).equals(AUDIO_VALUE_FRAMING)) {
|
|
778
|
+
pos += AUDIO_VALUE_FRAMING.length;
|
|
779
|
+
} else {
|
|
780
|
+
const scanEnd = Math.min(pos + 8, blob.length);
|
|
781
|
+
let found = -1;
|
|
782
|
+
for (let i = pos; i < scanEnd; i++) {
|
|
783
|
+
const b = blob[i];
|
|
784
|
+
if (b === 129 || b === 130 || b <= 128) {
|
|
785
|
+
found = i;
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (found === -1) return void 0;
|
|
790
|
+
pos = found;
|
|
791
|
+
}
|
|
792
|
+
if (pos >= blob.length) return void 0;
|
|
793
|
+
const lenByte = blob[pos++];
|
|
794
|
+
let length;
|
|
795
|
+
if (lenByte === 129) {
|
|
796
|
+
if (pos + 2 > blob.length) return void 0;
|
|
797
|
+
length = blob.readUInt16LE(pos);
|
|
798
|
+
pos += 2;
|
|
799
|
+
} else if (lenByte === 130) {
|
|
800
|
+
if (pos + 4 > blob.length) return void 0;
|
|
801
|
+
length = blob.readUInt32LE(pos);
|
|
802
|
+
pos += 4;
|
|
803
|
+
} else {
|
|
804
|
+
length = lenByte;
|
|
805
|
+
}
|
|
806
|
+
if (length <= 0 || pos + length > blob.length) return void 0;
|
|
807
|
+
const text = blob.toString("utf8", pos, pos + length).trim();
|
|
808
|
+
if (!text || !/[\p{L}\p{N}]/u.test(text)) return void 0;
|
|
809
|
+
return text;
|
|
810
|
+
}
|
|
770
811
|
function extractAttributedBodyText(blob) {
|
|
771
812
|
if (!blob) return void 0;
|
|
772
813
|
const native = getNativeParser();
|
|
@@ -819,6 +860,63 @@ function extractAttributedBodyText(blob) {
|
|
|
819
860
|
const best = [...candidates.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
820
861
|
return best?.[0];
|
|
821
862
|
}
|
|
863
|
+
function mergeDuplicateConversations(prepared) {
|
|
864
|
+
const merged = [];
|
|
865
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
866
|
+
for (const entry of prepared) {
|
|
867
|
+
const existingIndex = indexByKey.get(entry.mergeKey);
|
|
868
|
+
if (existingIndex === void 0) {
|
|
869
|
+
indexByKey.set(entry.mergeKey, merged.length);
|
|
870
|
+
merged.push(entry);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
merged[existingIndex] = mergeConversationEntries(merged[existingIndex], entry);
|
|
874
|
+
}
|
|
875
|
+
return merged;
|
|
876
|
+
}
|
|
877
|
+
function mergeConversationEntries(left, right) {
|
|
878
|
+
const preferred = pickPreferredConversationEntry(left, right);
|
|
879
|
+
const other = preferred === left ? right : left;
|
|
880
|
+
const sameIdentifier = preferred.conversation.chatIdentifier === other.conversation.chatIdentifier;
|
|
881
|
+
return {
|
|
882
|
+
mergeKey: preferred.mergeKey,
|
|
883
|
+
last: preferred.last ?? other.last,
|
|
884
|
+
conversation: {
|
|
885
|
+
...preferred.conversation,
|
|
886
|
+
displayName: preferred.conversation.displayName ?? other.conversation.displayName,
|
|
887
|
+
participants: [
|
|
888
|
+
.../* @__PURE__ */ new Set([...preferred.conversation.participants, ...other.conversation.participants])
|
|
889
|
+
],
|
|
890
|
+
unreadCount: sameIdentifier ? Math.max(preferred.conversation.unreadCount, other.conversation.unreadCount) : preferred.conversation.unreadCount + other.conversation.unreadCount
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function pickPreferredConversationEntry(left, right) {
|
|
895
|
+
const leftTime = left.conversation.lastMessageDate?.getTime() ?? 0;
|
|
896
|
+
const rightTime = right.conversation.lastMessageDate?.getTime() ?? 0;
|
|
897
|
+
if (leftTime !== rightTime) {
|
|
898
|
+
return leftTime > rightTime ? left : right;
|
|
899
|
+
}
|
|
900
|
+
const preferredService = left.last?.lastService ?? right.last?.lastService ?? null;
|
|
901
|
+
if (preferredService) {
|
|
902
|
+
const preferredType = preferredService.toLowerCase().includes("sms") ? "SMS" : "iMessage";
|
|
903
|
+
if (left.conversation.serviceType === preferredType && right.conversation.serviceType !== preferredType) {
|
|
904
|
+
return left;
|
|
905
|
+
}
|
|
906
|
+
if (right.conversation.serviceType === preferredType && left.conversation.serviceType !== preferredType) {
|
|
907
|
+
return right;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (left.conversation.displayName && !right.conversation.displayName) return left;
|
|
911
|
+
if (right.conversation.displayName && !left.conversation.displayName) return right;
|
|
912
|
+
if (left.conversation.serviceType === "iMessage" && right.conversation.serviceType === "SMS") {
|
|
913
|
+
return left;
|
|
914
|
+
}
|
|
915
|
+
if (right.conversation.serviceType === "iMessage" && left.conversation.serviceType === "SMS") {
|
|
916
|
+
return right;
|
|
917
|
+
}
|
|
918
|
+
return left;
|
|
919
|
+
}
|
|
822
920
|
function normalizeSnippetText(text) {
|
|
823
921
|
if (!text) return null;
|
|
824
922
|
const attachmentMarker = new RegExp(
|
|
@@ -1162,6 +1260,8 @@ class IMessageDB {
|
|
|
1162
1260
|
* other clear site. Add one if you introduce a read path that bypasses it.
|
|
1163
1261
|
*/
|
|
1164
1262
|
cachedReactionsByChat = /* @__PURE__ */ new Map();
|
|
1263
|
+
/** Per-table column-name sets, for optional-column guards (schema drift / synthetic fixtures). */
|
|
1264
|
+
columnCache = /* @__PURE__ */ new Map();
|
|
1165
1265
|
backgroundSyncNeeded = true;
|
|
1166
1266
|
backgroundRefreshScheduled = false;
|
|
1167
1267
|
/** Set on close() so background chunked work stops touching a closed DB. */
|
|
@@ -1771,12 +1871,12 @@ class IMessageDB {
|
|
|
1771
1871
|
const CHUNK = Math.max(limit, 200);
|
|
1772
1872
|
const prepared = [];
|
|
1773
1873
|
let cursor = 0;
|
|
1774
|
-
let deduped =
|
|
1874
|
+
let deduped = mergeDuplicateConversations(prepared);
|
|
1775
1875
|
while (deduped.length < limit && cursor < sortEntries.length) {
|
|
1776
1876
|
const chunk = sortEntries.slice(cursor, cursor + CHUNK);
|
|
1777
1877
|
cursor += chunk.length;
|
|
1778
1878
|
for (const entry of chunk) prepared.push(enrich(entry));
|
|
1779
|
-
deduped =
|
|
1879
|
+
deduped = mergeDuplicateConversations(prepared);
|
|
1780
1880
|
}
|
|
1781
1881
|
const selected = deduped.slice(0, limit);
|
|
1782
1882
|
const result = selected.map(({ conversation, last }) => ({
|
|
@@ -2091,63 +2191,6 @@ class IMessageDB {
|
|
|
2091
2191
|
}
|
|
2092
2192
|
return null;
|
|
2093
2193
|
}
|
|
2094
|
-
mergeDuplicateConversations(prepared) {
|
|
2095
|
-
const merged = [];
|
|
2096
|
-
const indexByKey = /* @__PURE__ */ new Map();
|
|
2097
|
-
for (const entry of prepared) {
|
|
2098
|
-
const existingIndex = indexByKey.get(entry.mergeKey);
|
|
2099
|
-
if (existingIndex === void 0) {
|
|
2100
|
-
indexByKey.set(entry.mergeKey, merged.length);
|
|
2101
|
-
merged.push(entry);
|
|
2102
|
-
continue;
|
|
2103
|
-
}
|
|
2104
|
-
merged[existingIndex] = this.mergeConversationEntries(merged[existingIndex], entry);
|
|
2105
|
-
}
|
|
2106
|
-
return merged;
|
|
2107
|
-
}
|
|
2108
|
-
mergeConversationEntries(left, right) {
|
|
2109
|
-
const preferred = this.pickPreferredConversationEntry(left, right);
|
|
2110
|
-
const other = preferred === left ? right : left;
|
|
2111
|
-
const sameIdentifier = preferred.conversation.chatIdentifier === other.conversation.chatIdentifier;
|
|
2112
|
-
return {
|
|
2113
|
-
mergeKey: preferred.mergeKey,
|
|
2114
|
-
last: preferred.last ?? other.last,
|
|
2115
|
-
conversation: {
|
|
2116
|
-
...preferred.conversation,
|
|
2117
|
-
displayName: preferred.conversation.displayName ?? other.conversation.displayName,
|
|
2118
|
-
participants: [
|
|
2119
|
-
.../* @__PURE__ */ new Set([...preferred.conversation.participants, ...other.conversation.participants])
|
|
2120
|
-
],
|
|
2121
|
-
unreadCount: sameIdentifier ? Math.max(preferred.conversation.unreadCount, other.conversation.unreadCount) : preferred.conversation.unreadCount + other.conversation.unreadCount
|
|
2122
|
-
}
|
|
2123
|
-
};
|
|
2124
|
-
}
|
|
2125
|
-
pickPreferredConversationEntry(left, right) {
|
|
2126
|
-
const leftTime = left.conversation.lastMessageDate?.getTime() ?? 0;
|
|
2127
|
-
const rightTime = right.conversation.lastMessageDate?.getTime() ?? 0;
|
|
2128
|
-
if (leftTime !== rightTime) {
|
|
2129
|
-
return leftTime > rightTime ? left : right;
|
|
2130
|
-
}
|
|
2131
|
-
const preferredService = left.last?.lastService ?? right.last?.lastService ?? null;
|
|
2132
|
-
if (preferredService) {
|
|
2133
|
-
const preferredType = preferredService.toLowerCase().includes("sms") ? "SMS" : "iMessage";
|
|
2134
|
-
if (left.conversation.serviceType === preferredType && right.conversation.serviceType !== preferredType) {
|
|
2135
|
-
return left;
|
|
2136
|
-
}
|
|
2137
|
-
if (right.conversation.serviceType === preferredType && left.conversation.serviceType !== preferredType) {
|
|
2138
|
-
return right;
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
if (left.conversation.displayName && !right.conversation.displayName) return left;
|
|
2142
|
-
if (right.conversation.displayName && !left.conversation.displayName) return right;
|
|
2143
|
-
if (left.conversation.serviceType === "iMessage" && right.conversation.serviceType === "SMS") {
|
|
2144
|
-
return left;
|
|
2145
|
-
}
|
|
2146
|
-
if (right.conversation.serviceType === "iMessage" && left.conversation.serviceType === "SMS") {
|
|
2147
|
-
return right;
|
|
2148
|
-
}
|
|
2149
|
-
return left;
|
|
2150
|
-
}
|
|
2151
2194
|
getConversationMergeKey(chatIdentifier, chatGuid, isGroup) {
|
|
2152
2195
|
const cacheKey = `${chatIdentifier}::${chatGuid}::${isGroup}`;
|
|
2153
2196
|
const cached = this.cachedMergeKeys.get(cacheKey);
|
|
@@ -2486,11 +2529,7 @@ class IMessageDB {
|
|
|
2486
2529
|
const isReply = Boolean(ext.thread_originator_guid);
|
|
2487
2530
|
let replyTo;
|
|
2488
2531
|
if (isReply && ext.thread_originator_guid) {
|
|
2489
|
-
|
|
2490
|
-
replyTo = {
|
|
2491
|
-
replyToGuid: ext.thread_originator_guid,
|
|
2492
|
-
replyToText: originalText
|
|
2493
|
-
};
|
|
2532
|
+
replyTo = this.getReplyContextByGuid(ext.thread_originator_guid);
|
|
2494
2533
|
}
|
|
2495
2534
|
const richContentType = getRichContentType(ext.balloon_bundle_id || null);
|
|
2496
2535
|
const hasAttachments = Boolean(ext.cache_has_attachments);
|
|
@@ -2520,6 +2559,7 @@ class IMessageDB {
|
|
|
2520
2559
|
associatedMessageType: associatedType,
|
|
2521
2560
|
hasAttachments
|
|
2522
2561
|
});
|
|
2562
|
+
const appleAudioTranscript = extractAudioTranscription(raw.attributedBody ?? null);
|
|
2523
2563
|
return {
|
|
2524
2564
|
id: raw.ROWID,
|
|
2525
2565
|
guid: raw.guid,
|
|
@@ -2547,6 +2587,7 @@ class IMessageDB {
|
|
|
2547
2587
|
richContentSummary,
|
|
2548
2588
|
isEdited: !isUnsent && Boolean(ext.date_edited && ext.date_edited > 0),
|
|
2549
2589
|
isRetracted: isUnsent || Boolean(ext.date_retracted && ext.date_retracted > 0),
|
|
2590
|
+
appleAudioTranscript,
|
|
2550
2591
|
hasAttachments,
|
|
2551
2592
|
attachments
|
|
2552
2593
|
};
|
|
@@ -2674,6 +2715,24 @@ class IMessageDB {
|
|
|
2674
2715
|
}
|
|
2675
2716
|
return result;
|
|
2676
2717
|
}
|
|
2718
|
+
/**
|
|
2719
|
+
* Whether `table` has `column` in the current chat.db schema (cached per table).
|
|
2720
|
+
* Guards columns that are absent on older macOS DBs or synthetic fixtures
|
|
2721
|
+
* (e.g. `message.is_audio_message`, `attachment.emoji_image_short_description`).
|
|
2722
|
+
*/
|
|
2723
|
+
hasColumn(table, column) {
|
|
2724
|
+
let cols = this.columnCache.get(table);
|
|
2725
|
+
if (!cols) {
|
|
2726
|
+
try {
|
|
2727
|
+
const rows = this.raw.prepare(`PRAGMA table_info(${table})`).all();
|
|
2728
|
+
cols = new Set(rows.map((r) => r.name));
|
|
2729
|
+
} catch {
|
|
2730
|
+
cols = /* @__PURE__ */ new Set();
|
|
2731
|
+
}
|
|
2732
|
+
this.columnCache.set(table, cols);
|
|
2733
|
+
}
|
|
2734
|
+
return cols.has(column);
|
|
2735
|
+
}
|
|
2677
2736
|
/** Max ROWID currently in the message table — used as a cache key. */
|
|
2678
2737
|
getMaxMessageRowId() {
|
|
2679
2738
|
const row = this.raw.prepare(`SELECT COALESCE(MAX(ROWID), 0) AS m FROM ${Tables.MESSAGE}`).get();
|
|
@@ -2869,13 +2928,14 @@ class IMessageDB {
|
|
|
2869
2928
|
* Fetch attachments for a message
|
|
2870
2929
|
*/
|
|
2871
2930
|
fetchAttachments(messageRowId) {
|
|
2931
|
+
const emojiCol = this.hasColumn("attachment", "emoji_image_short_description") ? ", a.emoji_image_short_description as emoji_desc" : "";
|
|
2872
2932
|
const stmt = this.raw.prepare(`
|
|
2873
|
-
SELECT
|
|
2933
|
+
SELECT
|
|
2874
2934
|
a.ROWID as row_id,
|
|
2875
2935
|
a.filename,
|
|
2876
2936
|
a.mime_type,
|
|
2877
2937
|
a.transfer_name,
|
|
2878
|
-
a.total_bytes
|
|
2938
|
+
a.total_bytes${emojiCol}
|
|
2879
2939
|
FROM ${Tables.ATTACHMENT} a
|
|
2880
2940
|
JOIN ${Tables.MESSAGE_ATTACHMENT_JOIN} maj ON a.ROWID = maj.attachment_id
|
|
2881
2941
|
WHERE maj.message_id = ?
|
|
@@ -2886,7 +2946,8 @@ class IMessageDB {
|
|
|
2886
2946
|
filename: r.filename || "",
|
|
2887
2947
|
mimeType: r.mime_type,
|
|
2888
2948
|
transferName: r.transfer_name,
|
|
2889
|
-
totalBytes: r.total_bytes || 0
|
|
2949
|
+
totalBytes: r.total_bytes || 0,
|
|
2950
|
+
emojiDescription: r.emoji_desc ?? null
|
|
2890
2951
|
}));
|
|
2891
2952
|
}
|
|
2892
2953
|
/**
|
|
@@ -2998,9 +3059,53 @@ class IMessageDB {
|
|
|
2998
3059
|
const row = this.raw.prepare(`SELECT ROWID, text, attributedBody FROM ${Tables.MESSAGE} WHERE ROWID = ? LIMIT 1`).get(rowId);
|
|
2999
3060
|
return this.extractMessageText(row);
|
|
3000
3061
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3062
|
+
/**
|
|
3063
|
+
* Build reply context for the message being replied to, by GUID. Beyond the
|
|
3064
|
+
* plain text, this resolves what KIND of message it was when it carries no text
|
|
3065
|
+
* of its own — a voice note (surfacing Apple's transcript), image, video, or
|
|
3066
|
+
* file — so the UI renders "↩ voice note: '…'" instead of a bare "(unknown)".
|
|
3067
|
+
*/
|
|
3068
|
+
getReplyContextByGuid(guid) {
|
|
3069
|
+
const audioCol = this.hasColumn("message", "is_audio_message") ? "is_audio_message" : "0 AS is_audio_message";
|
|
3070
|
+
const row = this.raw.prepare(
|
|
3071
|
+
`SELECT ROWID, text, attributedBody, ${audioCol}, cache_has_attachments
|
|
3072
|
+
FROM ${Tables.MESSAGE} WHERE guid = ? LIMIT 1`
|
|
3073
|
+
).get(guid);
|
|
3074
|
+
const ctx = { replyToGuid: guid };
|
|
3075
|
+
if (!row) return ctx;
|
|
3076
|
+
if (row.is_audio_message) {
|
|
3077
|
+
ctx.replyToKind = "voice-note";
|
|
3078
|
+
ctx.replyToText = extractAudioTranscription(row.attributedBody) ?? null;
|
|
3079
|
+
return ctx;
|
|
3080
|
+
}
|
|
3081
|
+
const text = this.extractMessageText(row);
|
|
3082
|
+
if (text) {
|
|
3083
|
+
ctx.replyToText = text;
|
|
3084
|
+
return ctx;
|
|
3085
|
+
}
|
|
3086
|
+
if (row.cache_has_attachments) {
|
|
3087
|
+
ctx.replyToKind = this.deriveAttachmentReplyKind(row.ROWID);
|
|
3088
|
+
ctx.replyToText = null;
|
|
3089
|
+
return ctx;
|
|
3090
|
+
}
|
|
3091
|
+
ctx.replyToText = null;
|
|
3092
|
+
return ctx;
|
|
3093
|
+
}
|
|
3094
|
+
/** Map a message's first (non-sticker) attachment mime/uti to a reply-kind label. */
|
|
3095
|
+
deriveAttachmentReplyKind(messageRowId) {
|
|
3096
|
+
const utiCol = this.hasColumn("attachment", "uti") ? "a.uti" : "NULL AS uti";
|
|
3097
|
+
const row = this.raw.prepare(
|
|
3098
|
+
`SELECT a.mime_type, ${utiCol}
|
|
3099
|
+
FROM ${Tables.ATTACHMENT} a
|
|
3100
|
+
JOIN ${Tables.MESSAGE_ATTACHMENT_JOIN} maj ON a.ROWID = maj.attachment_id
|
|
3101
|
+
WHERE maj.message_id = ? AND a.is_sticker = 0
|
|
3102
|
+
ORDER BY a.ROWID LIMIT 1`
|
|
3103
|
+
).get(messageRowId);
|
|
3104
|
+
const mime = (row?.mime_type ?? "").toLowerCase();
|
|
3105
|
+
const uti = (row?.uti ?? "").toLowerCase();
|
|
3106
|
+
if (mime.startsWith("image/") || uti.includes("image")) return "image";
|
|
3107
|
+
if (mime.startsWith("video/") || uti.includes("movie") || uti.includes("video")) return "video";
|
|
3108
|
+
return "file";
|
|
3004
3109
|
}
|
|
3005
3110
|
extractMessageText(row) {
|
|
3006
3111
|
if (!row) return null;
|
|
@@ -3019,4 +3124,4 @@ export {
|
|
|
3019
3124
|
normalizedPhoneVariants as n,
|
|
3020
3125
|
rankFuzzy as r
|
|
3021
3126
|
};
|
|
3022
|
-
//# sourceMappingURL=imessage-db-
|
|
3127
|
+
//# sourceMappingURL=imessage-db-vrzopRGT.js.map
|