@elizaos/plugin-imessage 2.0.0-alpha.3
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/__tests__/integration.test.ts +548 -0
- package/build.ts +16 -0
- package/dist/index.js +46 -0
- package/package.json +33 -0
- package/src/accounts.ts +379 -0
- package/src/actions/index.ts +5 -0
- package/src/actions/sendMessage.ts +218 -0
- package/src/config.ts +82 -0
- package/src/index.ts +113 -0
- package/src/providers/chatContext.ts +86 -0
- package/src/providers/index.ts +5 -0
- package/src/rpc.ts +485 -0
- package/src/service.ts +589 -0
- package/src/types.ts +291 -0
- package/tsconfig.json +20 -0
package/src/service.ts
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iMessage service implementation for ElizaOS.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exec } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { platform } from "node:os";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import {
|
|
10
|
+
type EventPayload,
|
|
11
|
+
type IAgentRuntime,
|
|
12
|
+
logger,
|
|
13
|
+
Service,
|
|
14
|
+
} from "@elizaos/core";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
17
|
+
formatPhoneNumber,
|
|
18
|
+
type IIMessageService,
|
|
19
|
+
IMESSAGE_SERVICE_NAME,
|
|
20
|
+
type IMessageChat,
|
|
21
|
+
type IMessageChatType,
|
|
22
|
+
IMessageCliError,
|
|
23
|
+
IMessageConfigurationError,
|
|
24
|
+
IMessageEventTypes,
|
|
25
|
+
type IMessageMessage,
|
|
26
|
+
IMessageNotSupportedError,
|
|
27
|
+
type IMessageSendOptions,
|
|
28
|
+
type IMessageSendResult,
|
|
29
|
+
type IMessageSettings,
|
|
30
|
+
isPhoneNumber,
|
|
31
|
+
splitMessageForIMessage,
|
|
32
|
+
} from "./types.js";
|
|
33
|
+
|
|
34
|
+
const execAsync = promisify(exec);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* iMessage service for ElizaOS agents.
|
|
38
|
+
* Note: This only works on macOS.
|
|
39
|
+
*/
|
|
40
|
+
export class IMessageService extends Service implements IIMessageService {
|
|
41
|
+
static serviceType: string = IMESSAGE_SERVICE_NAME;
|
|
42
|
+
|
|
43
|
+
capabilityDescription =
|
|
44
|
+
"iMessage service for sending and receiving messages on macOS";
|
|
45
|
+
|
|
46
|
+
private settings: IMessageSettings | null = null;
|
|
47
|
+
private connected: boolean = false;
|
|
48
|
+
private pollInterval: NodeJS.Timeout | null = null;
|
|
49
|
+
private lastMessageId: string | null = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Start the iMessage service.
|
|
53
|
+
*/
|
|
54
|
+
static async start(runtime: IAgentRuntime): Promise<IMessageService> {
|
|
55
|
+
logger.info("Starting iMessage service...");
|
|
56
|
+
|
|
57
|
+
const service = new IMessageService(runtime);
|
|
58
|
+
|
|
59
|
+
// Check if running on macOS
|
|
60
|
+
if (!service.isMacOS()) {
|
|
61
|
+
throw new IMessageNotSupportedError();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Load settings
|
|
65
|
+
service.settings = service.loadSettings();
|
|
66
|
+
await service.validateSettings();
|
|
67
|
+
|
|
68
|
+
// Start polling for new messages
|
|
69
|
+
if (service.settings.pollIntervalMs > 0) {
|
|
70
|
+
service.startPolling();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
service.connected = true;
|
|
74
|
+
logger.info("iMessage service started");
|
|
75
|
+
|
|
76
|
+
// Emit connection ready event
|
|
77
|
+
runtime.emitEvent(IMessageEventTypes.CONNECTION_READY, {
|
|
78
|
+
runtime,
|
|
79
|
+
service,
|
|
80
|
+
} as EventPayload);
|
|
81
|
+
|
|
82
|
+
return service;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stop the iMessage service.
|
|
87
|
+
*/
|
|
88
|
+
async stop(): Promise<void> {
|
|
89
|
+
logger.info("Stopping iMessage service...");
|
|
90
|
+
this.connected = false;
|
|
91
|
+
|
|
92
|
+
if (this.pollInterval) {
|
|
93
|
+
clearInterval(this.pollInterval);
|
|
94
|
+
this.pollInterval = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.settings = null;
|
|
98
|
+
this.lastMessageId = null;
|
|
99
|
+
logger.info("iMessage service stopped");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if the service is connected.
|
|
104
|
+
*/
|
|
105
|
+
isConnected(): boolean {
|
|
106
|
+
return this.connected;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if running on macOS.
|
|
111
|
+
*/
|
|
112
|
+
isMacOS(): boolean {
|
|
113
|
+
return platform() === "darwin";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send a message via iMessage.
|
|
118
|
+
*/
|
|
119
|
+
async sendMessage(
|
|
120
|
+
to: string,
|
|
121
|
+
text: string,
|
|
122
|
+
options?: IMessageSendOptions,
|
|
123
|
+
): Promise<IMessageSendResult> {
|
|
124
|
+
if (!this.settings) {
|
|
125
|
+
return { success: false, error: "Service not initialized" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Format phone number if needed
|
|
129
|
+
const target = isPhoneNumber(to) ? formatPhoneNumber(to) : to;
|
|
130
|
+
|
|
131
|
+
// Split message if too long
|
|
132
|
+
const chunks = splitMessageForIMessage(text);
|
|
133
|
+
|
|
134
|
+
for (const chunk of chunks) {
|
|
135
|
+
const result = await this.sendSingleMessage(target, chunk, options);
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Emit sent event
|
|
142
|
+
if (this.runtime) {
|
|
143
|
+
this.runtime.emitEvent(IMessageEventTypes.MESSAGE_SENT, {
|
|
144
|
+
runtime: this.runtime,
|
|
145
|
+
to: target,
|
|
146
|
+
text,
|
|
147
|
+
hasMedia: Boolean(options?.mediaUrl),
|
|
148
|
+
} as EventPayload);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
success: true,
|
|
153
|
+
messageId: Date.now().toString(),
|
|
154
|
+
chatId: target,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get recent messages.
|
|
160
|
+
*/
|
|
161
|
+
async getRecentMessages(limit: number = 50): Promise<IMessageMessage[]> {
|
|
162
|
+
if (!this.settings) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Use CLI or AppleScript to get recent messages
|
|
167
|
+
const script = `
|
|
168
|
+
tell application "Messages"
|
|
169
|
+
set AppleScript's text item delimiters to tab
|
|
170
|
+
set outputLines to {}
|
|
171
|
+
repeat with i from 1 to ${limit}
|
|
172
|
+
try
|
|
173
|
+
set msg to item i of (get messages)
|
|
174
|
+
set msgLine to (id of msg) & tab & (text of msg) & tab & ((date of msg) as string) & tab & (is_from_me of msg as string) & tab & (chat_identifier of msg as string) & tab & (handle of sender of msg)
|
|
175
|
+
set end of outputLines to msgLine
|
|
176
|
+
end try
|
|
177
|
+
end repeat
|
|
178
|
+
set AppleScript's text item delimiters to linefeed
|
|
179
|
+
set outputText to outputLines as string
|
|
180
|
+
set AppleScript's text item delimiters to ""
|
|
181
|
+
return outputText
|
|
182
|
+
end tell
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const result = await this.runAppleScript(script);
|
|
187
|
+
// Parse result and return messages
|
|
188
|
+
return this.parseMessagesResult(result);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.warn(`Failed to get recent messages: ${error}`);
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get chats.
|
|
197
|
+
*/
|
|
198
|
+
async getChats(): Promise<IMessageChat[]> {
|
|
199
|
+
if (!this.settings) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const script = `
|
|
204
|
+
tell application "Messages"
|
|
205
|
+
set chatList to {}
|
|
206
|
+
repeat with c in chats
|
|
207
|
+
set chatId to id of c
|
|
208
|
+
set chatName to name of c
|
|
209
|
+
set end of chatList to {chatId, chatName}
|
|
210
|
+
end repeat
|
|
211
|
+
return chatList
|
|
212
|
+
end tell
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const result = await this.runAppleScript(script);
|
|
217
|
+
return this.parseChatsResult(result);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.warn(`Failed to get chats: ${error}`);
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get current settings.
|
|
226
|
+
*/
|
|
227
|
+
getSettings(): IMessageSettings | null {
|
|
228
|
+
return this.settings;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Private methods
|
|
232
|
+
|
|
233
|
+
private loadSettings(): IMessageSettings {
|
|
234
|
+
if (!this.runtime) {
|
|
235
|
+
throw new IMessageConfigurationError("Runtime not initialized");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const getStringSetting = (
|
|
239
|
+
key: string,
|
|
240
|
+
envKey: string,
|
|
241
|
+
defaultValue = "",
|
|
242
|
+
): string => {
|
|
243
|
+
const value = this.runtime?.getSetting(key);
|
|
244
|
+
if (typeof value === "string") return value;
|
|
245
|
+
return process.env[envKey] || defaultValue;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const cliPath = getStringSetting(
|
|
249
|
+
"IMESSAGE_CLI_PATH",
|
|
250
|
+
"IMESSAGE_CLI_PATH",
|
|
251
|
+
"imsg",
|
|
252
|
+
);
|
|
253
|
+
const dbPath =
|
|
254
|
+
getStringSetting("IMESSAGE_DB_PATH", "IMESSAGE_DB_PATH") || undefined;
|
|
255
|
+
|
|
256
|
+
const pollIntervalMs =
|
|
257
|
+
Number(
|
|
258
|
+
getStringSetting(
|
|
259
|
+
"IMESSAGE_POLL_INTERVAL_MS",
|
|
260
|
+
"IMESSAGE_POLL_INTERVAL_MS",
|
|
261
|
+
),
|
|
262
|
+
) || DEFAULT_POLL_INTERVAL_MS;
|
|
263
|
+
|
|
264
|
+
const dmPolicy = getStringSetting(
|
|
265
|
+
"IMESSAGE_DM_POLICY",
|
|
266
|
+
"IMESSAGE_DM_POLICY",
|
|
267
|
+
"pairing",
|
|
268
|
+
) as IMessageSettings["dmPolicy"];
|
|
269
|
+
|
|
270
|
+
const groupPolicy = getStringSetting(
|
|
271
|
+
"IMESSAGE_GROUP_POLICY",
|
|
272
|
+
"IMESSAGE_GROUP_POLICY",
|
|
273
|
+
"allowlist",
|
|
274
|
+
) as IMessageSettings["groupPolicy"];
|
|
275
|
+
|
|
276
|
+
const allowFromRaw = getStringSetting(
|
|
277
|
+
"IMESSAGE_ALLOW_FROM",
|
|
278
|
+
"IMESSAGE_ALLOW_FROM",
|
|
279
|
+
);
|
|
280
|
+
const allowFrom = allowFromRaw
|
|
281
|
+
? allowFromRaw
|
|
282
|
+
.split(",")
|
|
283
|
+
.map((s: string) => s.trim())
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
: [];
|
|
286
|
+
|
|
287
|
+
const enabledRaw = getStringSetting(
|
|
288
|
+
"IMESSAGE_ENABLED",
|
|
289
|
+
"IMESSAGE_ENABLED",
|
|
290
|
+
"true",
|
|
291
|
+
);
|
|
292
|
+
const enabled = enabledRaw !== "false";
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
cliPath,
|
|
296
|
+
dbPath,
|
|
297
|
+
pollIntervalMs,
|
|
298
|
+
dmPolicy,
|
|
299
|
+
groupPolicy,
|
|
300
|
+
allowFrom,
|
|
301
|
+
enabled,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private async validateSettings(): Promise<void> {
|
|
306
|
+
if (!this.settings) {
|
|
307
|
+
throw new IMessageConfigurationError("Settings not loaded");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check if CLI tool exists (if specified and not default)
|
|
311
|
+
if (this.settings.cliPath !== "imsg") {
|
|
312
|
+
if (!existsSync(this.settings.cliPath)) {
|
|
313
|
+
logger.warn(
|
|
314
|
+
`iMessage CLI not found at ${this.settings.cliPath}, will use AppleScript`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Check if Messages app is accessible
|
|
320
|
+
try {
|
|
321
|
+
await this.runAppleScript('tell application "Messages" to return 1');
|
|
322
|
+
} catch (_error) {
|
|
323
|
+
throw new IMessageConfigurationError(
|
|
324
|
+
"Cannot access Messages app. Ensure Full Disk Access is granted.",
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async sendSingleMessage(
|
|
330
|
+
to: string,
|
|
331
|
+
text: string,
|
|
332
|
+
options?: IMessageSendOptions,
|
|
333
|
+
): Promise<IMessageSendResult> {
|
|
334
|
+
// Try CLI first if available
|
|
335
|
+
if (this.settings?.cliPath && this.settings.cliPath !== "imsg") {
|
|
336
|
+
try {
|
|
337
|
+
return await this.sendViaCli(to, text, options);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
logger.debug(`CLI send failed, falling back to AppleScript: ${error}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Fall back to AppleScript
|
|
344
|
+
return await this.sendViaAppleScript(to, text, options);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private async sendViaCli(
|
|
348
|
+
to: string,
|
|
349
|
+
text: string,
|
|
350
|
+
options?: IMessageSendOptions,
|
|
351
|
+
): Promise<IMessageSendResult> {
|
|
352
|
+
if (!this.settings) {
|
|
353
|
+
return { success: false, error: "Service not initialized" };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const args = [to, text];
|
|
357
|
+
if (options?.mediaUrl) {
|
|
358
|
+
args.push("--attachment", options.mediaUrl);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
await execAsync(
|
|
363
|
+
`"${this.settings.cliPath}" ${args.map((a) => `"${a}"`).join(" ")}`,
|
|
364
|
+
);
|
|
365
|
+
return { success: true, messageId: Date.now().toString(), chatId: to };
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const err = error as { code?: number; message?: string };
|
|
368
|
+
throw new IMessageCliError(err.message || "CLI command failed", err.code);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async sendViaAppleScript(
|
|
373
|
+
to: string,
|
|
374
|
+
text: string,
|
|
375
|
+
_options?: IMessageSendOptions,
|
|
376
|
+
): Promise<IMessageSendResult> {
|
|
377
|
+
// Escape text for AppleScript
|
|
378
|
+
const escapedText = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
379
|
+
|
|
380
|
+
let script: string;
|
|
381
|
+
|
|
382
|
+
if (to.startsWith("chat_id:")) {
|
|
383
|
+
// Send to existing chat
|
|
384
|
+
const chatId = to.slice(8);
|
|
385
|
+
script = `
|
|
386
|
+
tell application "Messages"
|
|
387
|
+
set targetChat to chat id "${chatId}"
|
|
388
|
+
send "${escapedText}" to targetChat
|
|
389
|
+
end tell
|
|
390
|
+
`;
|
|
391
|
+
} else {
|
|
392
|
+
// Send to buddy (phone/email)
|
|
393
|
+
script = `
|
|
394
|
+
tell application "Messages"
|
|
395
|
+
set targetService to 1st account whose service type = iMessage
|
|
396
|
+
set targetBuddy to participant "${to}" of targetService
|
|
397
|
+
send "${escapedText}" to targetBuddy
|
|
398
|
+
end tell
|
|
399
|
+
`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
await this.runAppleScript(script);
|
|
404
|
+
return { success: true, messageId: Date.now().toString(), chatId: to };
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return { success: false, error: `AppleScript error: ${error}` };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async runAppleScript(script: string): Promise<string> {
|
|
411
|
+
try {
|
|
412
|
+
const { stdout } = await execAsync(
|
|
413
|
+
`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`,
|
|
414
|
+
);
|
|
415
|
+
return stdout.trim();
|
|
416
|
+
} catch (error) {
|
|
417
|
+
const err = error as { stderr?: string; message?: string };
|
|
418
|
+
throw new Error(
|
|
419
|
+
err.stderr || err.message || "AppleScript execution failed",
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private startPolling(): void {
|
|
425
|
+
if (!this.settings) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.pollInterval = setInterval(async () => {
|
|
430
|
+
try {
|
|
431
|
+
await this.pollForNewMessages();
|
|
432
|
+
} catch (error) {
|
|
433
|
+
logger.debug(`Polling error: ${error}`);
|
|
434
|
+
}
|
|
435
|
+
}, this.settings.pollIntervalMs);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async pollForNewMessages(): Promise<void> {
|
|
439
|
+
if (!this.runtime) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const messages = await this.getRecentMessages(10);
|
|
444
|
+
|
|
445
|
+
for (const msg of messages) {
|
|
446
|
+
// Skip if we've already seen this message
|
|
447
|
+
if (this.lastMessageId && msg.id <= this.lastMessageId) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Skip messages from self
|
|
452
|
+
if (msg.isFromMe) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check DM policy
|
|
457
|
+
if (!this.isAllowed(msg.handle)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Emit message received event
|
|
462
|
+
this.runtime.emitEvent(IMessageEventTypes.MESSAGE_RECEIVED, {
|
|
463
|
+
runtime: this.runtime,
|
|
464
|
+
message: msg,
|
|
465
|
+
} as EventPayload);
|
|
466
|
+
|
|
467
|
+
this.lastMessageId = msg.id;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private isAllowed(handle: string): boolean {
|
|
472
|
+
if (!this.settings) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (this.settings.dmPolicy === "open") {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (this.settings.dmPolicy === "disabled") {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (this.settings.dmPolicy === "allowlist") {
|
|
485
|
+
return this.settings.allowFrom.some(
|
|
486
|
+
(allowed) => allowed.toLowerCase() === handle.toLowerCase(),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// pairing - allow and track
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private parseMessagesResult(result: string): IMessageMessage[] {
|
|
495
|
+
return parseMessagesFromAppleScript(result);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private parseChatsResult(result: string): IMessageChat[] {
|
|
499
|
+
return parseChatsFromAppleScript(result);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Parse tab-delimited AppleScript messages output.
|
|
505
|
+
* Expected format per line: "id\ttext\tdate_sent\tis_from_me\tchat_identifier\tsender"
|
|
506
|
+
*/
|
|
507
|
+
export function parseMessagesFromAppleScript(
|
|
508
|
+
result: string,
|
|
509
|
+
): IMessageMessage[] {
|
|
510
|
+
const messages: IMessageMessage[] = [];
|
|
511
|
+
if (!result || !result.trim()) {
|
|
512
|
+
return messages;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const line of result.split("\n")) {
|
|
516
|
+
const trimmed = line.trim();
|
|
517
|
+
if (!trimmed) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const fields = trimmed.split("\t");
|
|
522
|
+
if (fields.length < 6) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const [id, text, dateSent, isFromMeStr, chatIdentifier, sender] = fields;
|
|
527
|
+
|
|
528
|
+
const isFromMe =
|
|
529
|
+
isFromMeStr === "1" || isFromMeStr.toLowerCase() === "true";
|
|
530
|
+
|
|
531
|
+
let timestamp: number;
|
|
532
|
+
const parsed = Number(dateSent);
|
|
533
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
534
|
+
timestamp = parsed;
|
|
535
|
+
} else {
|
|
536
|
+
const dateObj = new Date(dateSent);
|
|
537
|
+
timestamp = Number.isNaN(dateObj.getTime()) ? 0 : dateObj.getTime();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
messages.push({
|
|
541
|
+
id: id || "",
|
|
542
|
+
text: text || "",
|
|
543
|
+
handle: sender || "",
|
|
544
|
+
chatId: chatIdentifier || "",
|
|
545
|
+
timestamp,
|
|
546
|
+
isFromMe,
|
|
547
|
+
hasAttachments: false,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return messages;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Parse tab-delimited AppleScript chats output.
|
|
556
|
+
* Expected format per line: "chat_identifier\tdisplay_name\tparticipant_count\tlast_message_date"
|
|
557
|
+
*/
|
|
558
|
+
export function parseChatsFromAppleScript(result: string): IMessageChat[] {
|
|
559
|
+
const chats: IMessageChat[] = [];
|
|
560
|
+
if (!result || !result.trim()) {
|
|
561
|
+
return chats;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
for (const line of result.split("\n")) {
|
|
565
|
+
const trimmed = line.trim();
|
|
566
|
+
if (!trimmed) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const fields = trimmed.split("\t");
|
|
571
|
+
if (fields.length < 4) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const [chatIdentifier, displayName, participantCountStr] = fields;
|
|
576
|
+
|
|
577
|
+
const participantCount = Number(participantCountStr) || 0;
|
|
578
|
+
const chatType: IMessageChatType = participantCount > 1 ? "group" : "direct";
|
|
579
|
+
|
|
580
|
+
chats.push({
|
|
581
|
+
chatId: chatIdentifier || "",
|
|
582
|
+
chatType,
|
|
583
|
+
displayName: displayName || undefined,
|
|
584
|
+
participants: [],
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return chats;
|
|
589
|
+
}
|