imsg-mcp 1.7.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 +14 -0
- package/README.md +1 -1
- package/dist/cli.js +2 -2
- package/dist/{imessage-db-Cid38QGT.js → imessage-db-vrzopRGT.js} +221 -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-Lpz_PHJ5.js → meta-BrYyl7Ls.js} +2 -2
- package/dist/{meta-Lpz_PHJ5.js.map → meta-BrYyl7Ls.js.map} +1 -1
- package/dist/tui.js +405 -65
- 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-Cid38QGT.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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
|
+
|
|
13
|
+
# [1.8.0](https://github.com/george43g/imsg-mcp/compare/v1.7.0...v1.8.0) (2026-07-20)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **tui:** per-thread info / attachment drawer (press `i`) ([cbcf809](https://github.com/george43g/imsg-mcp/commit/cbcf80960207a081f5813a4982f94d927a2d7756))
|
|
19
|
+
|
|
6
20
|
# [1.7.0](https://github.com/george43g/imsg-mcp/compare/v1.6.2...v1.7.0) (2026-07-20)
|
|
7
21
|
|
|
8
22
|
|
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();
|
|
@@ -2804,6 +2863,51 @@ class IMessageDB {
|
|
|
2804
2863
|
span.end({ count: out.length });
|
|
2805
2864
|
return out;
|
|
2806
2865
|
}
|
|
2866
|
+
/**
|
|
2867
|
+
* All attachments across EVERY merged leg of one conversation (newest first)
|
|
2868
|
+
* for the TUI per-thread info drawer. Unlike searchAttachments (single
|
|
2869
|
+
* chat_identifier), this resolves the conversation to all its chat ROWIDs
|
|
2870
|
+
* (resolveChatsForConversation) so the SMS and iMessage legs of a merged
|
|
2871
|
+
* identity are both covered. Excludes stickers and Apple plugin payloads.
|
|
2872
|
+
*/
|
|
2873
|
+
listConversationAttachments(chatIdentifier, limit = 500) {
|
|
2874
|
+
const span = perf("listConversationAttachments");
|
|
2875
|
+
const chats = this.resolveChatsForConversation(chatIdentifier);
|
|
2876
|
+
if (chats.length === 0) {
|
|
2877
|
+
span.end({ returned: 0 });
|
|
2878
|
+
return [];
|
|
2879
|
+
}
|
|
2880
|
+
const placeholders = chats.map(() => "?").join(",");
|
|
2881
|
+
const lim = limit > 0 ? limit : 500;
|
|
2882
|
+
const sql = `
|
|
2883
|
+
SELECT DISTINCT
|
|
2884
|
+
a.ROWID as rowId,
|
|
2885
|
+
a.filename,
|
|
2886
|
+
a.mime_type,
|
|
2887
|
+
a.transfer_name,
|
|
2888
|
+
a.total_bytes,
|
|
2889
|
+
a.created_date
|
|
2890
|
+
FROM ${Tables.ATTACHMENT} a
|
|
2891
|
+
JOIN ${Tables.MESSAGE_ATTACHMENT_JOIN} maj ON a.ROWID = maj.attachment_id
|
|
2892
|
+
JOIN ${Tables.CHAT_MESSAGE_JOIN} cmj ON maj.message_id = cmj.message_id
|
|
2893
|
+
WHERE cmj.chat_id IN (${placeholders})
|
|
2894
|
+
AND a.is_sticker = 0
|
|
2895
|
+
AND (a.uti IS NULL OR a.uti NOT LIKE 'com.apple.messages.plugin%')
|
|
2896
|
+
ORDER BY a.created_date DESC
|
|
2897
|
+
LIMIT ?
|
|
2898
|
+
`;
|
|
2899
|
+
const rows = this.raw.prepare(sql).all(...chats.map((c) => c.ROWID), lim);
|
|
2900
|
+
const out = rows.map((r) => ({
|
|
2901
|
+
rowId: Number(r.rowId),
|
|
2902
|
+
filename: r.filename || "",
|
|
2903
|
+
mimeType: r.mime_type ?? null,
|
|
2904
|
+
transferName: r.transfer_name ?? null,
|
|
2905
|
+
totalBytes: Number(r.total_bytes) || 0,
|
|
2906
|
+
createdDate: macAutoTimestampToDate(Number(r.created_date)) ?? /* @__PURE__ */ new Date(0)
|
|
2907
|
+
}));
|
|
2908
|
+
span.end({ returned: out.length });
|
|
2909
|
+
return out;
|
|
2910
|
+
}
|
|
2807
2911
|
/** Fetch a single attachment record by ROWID. */
|
|
2808
2912
|
getAttachmentByRowId(rowId) {
|
|
2809
2913
|
const row = this.raw.prepare(
|
|
@@ -2824,13 +2928,14 @@ class IMessageDB {
|
|
|
2824
2928
|
* Fetch attachments for a message
|
|
2825
2929
|
*/
|
|
2826
2930
|
fetchAttachments(messageRowId) {
|
|
2931
|
+
const emojiCol = this.hasColumn("attachment", "emoji_image_short_description") ? ", a.emoji_image_short_description as emoji_desc" : "";
|
|
2827
2932
|
const stmt = this.raw.prepare(`
|
|
2828
|
-
SELECT
|
|
2933
|
+
SELECT
|
|
2829
2934
|
a.ROWID as row_id,
|
|
2830
2935
|
a.filename,
|
|
2831
2936
|
a.mime_type,
|
|
2832
2937
|
a.transfer_name,
|
|
2833
|
-
a.total_bytes
|
|
2938
|
+
a.total_bytes${emojiCol}
|
|
2834
2939
|
FROM ${Tables.ATTACHMENT} a
|
|
2835
2940
|
JOIN ${Tables.MESSAGE_ATTACHMENT_JOIN} maj ON a.ROWID = maj.attachment_id
|
|
2836
2941
|
WHERE maj.message_id = ?
|
|
@@ -2841,7 +2946,8 @@ class IMessageDB {
|
|
|
2841
2946
|
filename: r.filename || "",
|
|
2842
2947
|
mimeType: r.mime_type,
|
|
2843
2948
|
transferName: r.transfer_name,
|
|
2844
|
-
totalBytes: r.total_bytes || 0
|
|
2949
|
+
totalBytes: r.total_bytes || 0,
|
|
2950
|
+
emojiDescription: r.emoji_desc ?? null
|
|
2845
2951
|
}));
|
|
2846
2952
|
}
|
|
2847
2953
|
/**
|
|
@@ -2953,9 +3059,53 @@ class IMessageDB {
|
|
|
2953
3059
|
const row = this.raw.prepare(`SELECT ROWID, text, attributedBody FROM ${Tables.MESSAGE} WHERE ROWID = ? LIMIT 1`).get(rowId);
|
|
2954
3060
|
return this.extractMessageText(row);
|
|
2955
3061
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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";
|
|
2959
3109
|
}
|
|
2960
3110
|
extractMessageText(row) {
|
|
2961
3111
|
if (!row) return null;
|
|
@@ -2974,4 +3124,4 @@ export {
|
|
|
2974
3124
|
normalizedPhoneVariants as n,
|
|
2975
3125
|
rankFuzzy as r
|
|
2976
3126
|
};
|
|
2977
|
-
//# sourceMappingURL=imessage-db-
|
|
3127
|
+
//# sourceMappingURL=imessage-db-vrzopRGT.js.map
|