@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/index.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iMessage Plugin for ElizaOS
|
|
3
|
+
*
|
|
4
|
+
* Provides iMessage integration for ElizaOS agents on macOS.
|
|
5
|
+
* Uses AppleScript and/or CLI tools to send and receive messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { platform } from "node:os";
|
|
9
|
+
import type { IAgentRuntime, Plugin } from "@elizaos/core";
|
|
10
|
+
import { logger } from "@elizaos/core";
|
|
11
|
+
import { sendMessage } from "./actions/index.js";
|
|
12
|
+
import { chatContextProvider } from "./providers/index.js";
|
|
13
|
+
import {
|
|
14
|
+
IMessageService,
|
|
15
|
+
parseChatsFromAppleScript,
|
|
16
|
+
parseMessagesFromAppleScript,
|
|
17
|
+
} from "./service.js";
|
|
18
|
+
|
|
19
|
+
// Re-export types and service
|
|
20
|
+
export * from "./types.js";
|
|
21
|
+
export { IMessageService, parseMessagesFromAppleScript, parseChatsFromAppleScript };
|
|
22
|
+
export { sendMessage };
|
|
23
|
+
export { chatContextProvider };
|
|
24
|
+
|
|
25
|
+
// Account management exports
|
|
26
|
+
export {
|
|
27
|
+
DEFAULT_ACCOUNT_ID,
|
|
28
|
+
type IMessageAccountConfig,
|
|
29
|
+
type IMessageGroupConfig,
|
|
30
|
+
type IMessageMultiAccountConfig,
|
|
31
|
+
isIMessageMentionRequired,
|
|
32
|
+
isIMessageUserAllowed,
|
|
33
|
+
isMultiAccountEnabled,
|
|
34
|
+
listEnabledIMessageAccounts,
|
|
35
|
+
listIMessageAccountIds,
|
|
36
|
+
normalizeAccountId,
|
|
37
|
+
type ResolvedIMessageAccount,
|
|
38
|
+
resolveDefaultIMessageAccountId,
|
|
39
|
+
resolveIMessageAccount,
|
|
40
|
+
resolveIMessageGroupConfig,
|
|
41
|
+
} from "./accounts.js";
|
|
42
|
+
|
|
43
|
+
// RPC client exports
|
|
44
|
+
export {
|
|
45
|
+
createIMessageRpcClient,
|
|
46
|
+
DEFAULT_PROBE_TIMEOUT_MS,
|
|
47
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
48
|
+
getChatInfo,
|
|
49
|
+
getContactInfo,
|
|
50
|
+
getMessages,
|
|
51
|
+
type IMessageAttachment,
|
|
52
|
+
type IMessageChat,
|
|
53
|
+
type IMessageContact,
|
|
54
|
+
type IMessageMessage,
|
|
55
|
+
IMessageRpcClient,
|
|
56
|
+
type IMessageRpcClientOptions,
|
|
57
|
+
type IMessageRpcError,
|
|
58
|
+
type IMessageRpcNotification,
|
|
59
|
+
type IMessageRpcResponse,
|
|
60
|
+
listChats,
|
|
61
|
+
listContacts,
|
|
62
|
+
probeIMessageRpc,
|
|
63
|
+
sendIMessageRpc,
|
|
64
|
+
} from "./rpc.js";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* iMessage plugin for ElizaOS agents.
|
|
68
|
+
*/
|
|
69
|
+
const imessagePlugin: Plugin = {
|
|
70
|
+
name: "imessage",
|
|
71
|
+
description: "iMessage plugin for ElizaOS agents (macOS only)",
|
|
72
|
+
|
|
73
|
+
services: [IMessageService],
|
|
74
|
+
actions: [sendMessage],
|
|
75
|
+
providers: [chatContextProvider],
|
|
76
|
+
tests: [],
|
|
77
|
+
|
|
78
|
+
init: async (
|
|
79
|
+
config: Record<string, string>,
|
|
80
|
+
_runtime: IAgentRuntime,
|
|
81
|
+
): Promise<void> => {
|
|
82
|
+
logger.info("Initializing iMessage plugin...");
|
|
83
|
+
|
|
84
|
+
const isMacOS = platform() === "darwin";
|
|
85
|
+
|
|
86
|
+
logger.info("iMessage plugin configuration:");
|
|
87
|
+
logger.info(` - Platform: ${platform()}`);
|
|
88
|
+
logger.info(` - macOS: ${isMacOS ? "Yes" : "No"}`);
|
|
89
|
+
logger.info(
|
|
90
|
+
` - CLI path: ${config.IMESSAGE_CLI_PATH || process.env.IMESSAGE_CLI_PATH || "imsg (default)"}`,
|
|
91
|
+
);
|
|
92
|
+
logger.info(
|
|
93
|
+
` - DM policy: ${config.IMESSAGE_DM_POLICY || process.env.IMESSAGE_DM_POLICY || "pairing"}`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!isMacOS) {
|
|
97
|
+
logger.warn(
|
|
98
|
+
"iMessage plugin is only supported on macOS. The plugin will be inactive on this platform.",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.info("iMessage plugin initialized");
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default imessagePlugin;
|
|
107
|
+
|
|
108
|
+
// Channel configuration types
|
|
109
|
+
export type {
|
|
110
|
+
IMessageAccountConfig,
|
|
111
|
+
IMessageConfig,
|
|
112
|
+
IMessageReactionNotificationMode,
|
|
113
|
+
} from "./config.js";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat context provider for the iMessage plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
IAgentRuntime,
|
|
7
|
+
Memory,
|
|
8
|
+
Provider,
|
|
9
|
+
ProviderResult,
|
|
10
|
+
State,
|
|
11
|
+
} from "@elizaos/core";
|
|
12
|
+
import type { IMessageService } from "../service.js";
|
|
13
|
+
import { IMESSAGE_SERVICE_NAME } from "../types.js";
|
|
14
|
+
|
|
15
|
+
export const chatContextProvider: Provider = {
|
|
16
|
+
name: "imessageChatContext",
|
|
17
|
+
description: "Provides information about the current iMessage chat context",
|
|
18
|
+
|
|
19
|
+
get: async (
|
|
20
|
+
runtime: IAgentRuntime,
|
|
21
|
+
message: Memory,
|
|
22
|
+
state: State,
|
|
23
|
+
): Promise<ProviderResult> => {
|
|
24
|
+
// Only provide context for iMessage messages
|
|
25
|
+
if (message.content.source !== "imessage") {
|
|
26
|
+
return {
|
|
27
|
+
data: {},
|
|
28
|
+
values: {},
|
|
29
|
+
text: "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const imessageService = runtime.getService<IMessageService>(
|
|
34
|
+
IMESSAGE_SERVICE_NAME,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!imessageService || !imessageService.isConnected()) {
|
|
38
|
+
return {
|
|
39
|
+
data: { connected: false },
|
|
40
|
+
values: { connected: false },
|
|
41
|
+
text: "",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const agentName = state?.agentName || "The agent";
|
|
46
|
+
const stateData = (state?.data || {}) as Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
const handle = stateData.handle as string | undefined;
|
|
49
|
+
const chatId = stateData.chatId as string | undefined;
|
|
50
|
+
const chatType = stateData.chatType as string | undefined;
|
|
51
|
+
const displayName = stateData.displayName as string | undefined;
|
|
52
|
+
|
|
53
|
+
let chatDescription = "";
|
|
54
|
+
if (chatType === "group") {
|
|
55
|
+
chatDescription = displayName
|
|
56
|
+
? `group chat "${displayName}"`
|
|
57
|
+
: "a group chat";
|
|
58
|
+
} else {
|
|
59
|
+
chatDescription = handle
|
|
60
|
+
? `direct message with ${handle}`
|
|
61
|
+
: "a direct message";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const responseText =
|
|
65
|
+
`${agentName} is chatting via iMessage in ${chatDescription}. ` +
|
|
66
|
+
"iMessage supports text messages and attachments.";
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
data: {
|
|
70
|
+
handle,
|
|
71
|
+
chatId,
|
|
72
|
+
chatType: chatType || "direct",
|
|
73
|
+
displayName,
|
|
74
|
+
connected: true,
|
|
75
|
+
platform: "imessage",
|
|
76
|
+
},
|
|
77
|
+
values: {
|
|
78
|
+
handle,
|
|
79
|
+
chatId,
|
|
80
|
+
chatType: chatType || "direct",
|
|
81
|
+
displayName,
|
|
82
|
+
},
|
|
83
|
+
text: responseText,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve as resolvePath } from "node:path";
|
|
4
|
+
import { createInterface, type Interface } from "node:readline";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default probe timeout in milliseconds
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_PROBE_TIMEOUT_MS = 5000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default request timeout in milliseconds
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* iMessage RPC error structure
|
|
18
|
+
*/
|
|
19
|
+
export interface IMessageRpcError {
|
|
20
|
+
code?: number;
|
|
21
|
+
message?: string;
|
|
22
|
+
data?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* iMessage RPC response structure
|
|
27
|
+
*/
|
|
28
|
+
export interface IMessageRpcResponse<T> {
|
|
29
|
+
jsonrpc?: string;
|
|
30
|
+
id?: string | number | null;
|
|
31
|
+
result?: T;
|
|
32
|
+
error?: IMessageRpcError;
|
|
33
|
+
method?: string;
|
|
34
|
+
params?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* iMessage RPC notification structure
|
|
39
|
+
*/
|
|
40
|
+
export interface IMessageRpcNotification {
|
|
41
|
+
method: string;
|
|
42
|
+
params?: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for creating an iMessage RPC client
|
|
47
|
+
*/
|
|
48
|
+
export interface IMessageRpcClientOptions {
|
|
49
|
+
cliPath?: string;
|
|
50
|
+
dbPath?: string;
|
|
51
|
+
onNotification?: (msg: IMessageRpcNotification) => void;
|
|
52
|
+
onError?: (error: Error) => void;
|
|
53
|
+
onClose?: (code: number | null, signal: NodeJS.Signals | null) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pending request tracking
|
|
58
|
+
*/
|
|
59
|
+
interface PendingRequest {
|
|
60
|
+
resolve: (value: unknown) => void;
|
|
61
|
+
reject: (error: Error) => void;
|
|
62
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a path with ~ expansion
|
|
67
|
+
*/
|
|
68
|
+
function resolveUserPath(path: string): string {
|
|
69
|
+
if (path.startsWith("~/")) {
|
|
70
|
+
return resolvePath(homedir(), path.slice(2));
|
|
71
|
+
}
|
|
72
|
+
return resolvePath(path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* iMessage RPC client for communicating with the imsg CLI tool
|
|
77
|
+
*/
|
|
78
|
+
export class IMessageRpcClient {
|
|
79
|
+
private readonly cliPath: string;
|
|
80
|
+
private readonly dbPath?: string;
|
|
81
|
+
private readonly onNotification?: (msg: IMessageRpcNotification) => void;
|
|
82
|
+
private readonly onError?: (error: Error) => void;
|
|
83
|
+
private readonly onClose?: (
|
|
84
|
+
code: number | null,
|
|
85
|
+
signal: NodeJS.Signals | null,
|
|
86
|
+
) => void;
|
|
87
|
+
private readonly pending = new Map<string, PendingRequest>();
|
|
88
|
+
private readonly closedPromise: Promise<void>;
|
|
89
|
+
private closedResolve: (() => void) | null = null;
|
|
90
|
+
private child: ChildProcessWithoutNullStreams | null = null;
|
|
91
|
+
private reader: Interface | null = null;
|
|
92
|
+
private nextId = 1;
|
|
93
|
+
private started = false;
|
|
94
|
+
|
|
95
|
+
constructor(opts: IMessageRpcClientOptions = {}) {
|
|
96
|
+
this.cliPath = opts.cliPath?.trim() || "imsg";
|
|
97
|
+
this.dbPath = opts.dbPath?.trim()
|
|
98
|
+
? resolveUserPath(opts.dbPath)
|
|
99
|
+
: undefined;
|
|
100
|
+
this.onNotification = opts.onNotification;
|
|
101
|
+
this.onError = opts.onError;
|
|
102
|
+
this.onClose = opts.onClose;
|
|
103
|
+
this.closedPromise = new Promise((resolve) => {
|
|
104
|
+
this.closedResolve = resolve;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Starts the RPC client by spawning the CLI process
|
|
110
|
+
*/
|
|
111
|
+
async start(): Promise<void> {
|
|
112
|
+
if (this.child) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const args = ["rpc"];
|
|
117
|
+
if (this.dbPath) {
|
|
118
|
+
args.push("--db", this.dbPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const child = spawn(this.cliPath, args, {
|
|
122
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.child = child;
|
|
126
|
+
this.started = true;
|
|
127
|
+
this.reader = createInterface({ input: child.stdout });
|
|
128
|
+
|
|
129
|
+
this.reader.on("line", (line) => {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (!trimmed) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.handleLine(trimmed);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
child.stderr?.on("data", (chunk) => {
|
|
138
|
+
const lines = chunk.toString().split(/\r?\n/);
|
|
139
|
+
for (const line of lines) {
|
|
140
|
+
if (!line.trim()) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
this.onError?.(new Error(`imsg rpc stderr: ${line.trim()}`));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.on("error", (err) => {
|
|
148
|
+
this.failAll(err instanceof Error ? err : new Error(String(err)));
|
|
149
|
+
this.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
150
|
+
this.closedResolve?.();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
child.on("close", (code, signal) => {
|
|
154
|
+
if (code !== 0 && code !== null) {
|
|
155
|
+
const reason = signal ? `signal ${signal}` : `code ${code}`;
|
|
156
|
+
this.failAll(new Error(`imsg rpc exited (${reason})`));
|
|
157
|
+
} else {
|
|
158
|
+
this.failAll(new Error("imsg rpc closed"));
|
|
159
|
+
}
|
|
160
|
+
this.onClose?.(code, signal);
|
|
161
|
+
this.closedResolve?.();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Stops the RPC client
|
|
167
|
+
*/
|
|
168
|
+
async stop(): Promise<void> {
|
|
169
|
+
if (!this.child) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.reader?.close();
|
|
174
|
+
this.reader = null;
|
|
175
|
+
this.child.stdin?.end();
|
|
176
|
+
|
|
177
|
+
const child = this.child;
|
|
178
|
+
this.child = null;
|
|
179
|
+
|
|
180
|
+
await Promise.race([
|
|
181
|
+
this.closedPromise,
|
|
182
|
+
new Promise<void>((resolve) => {
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
if (!child.killed) {
|
|
185
|
+
child.kill("SIGTERM");
|
|
186
|
+
}
|
|
187
|
+
resolve();
|
|
188
|
+
}, 500);
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Waits for the RPC client to close
|
|
195
|
+
*/
|
|
196
|
+
async waitForClose(): Promise<void> {
|
|
197
|
+
await this.closedPromise;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Checks if the client is running
|
|
202
|
+
*/
|
|
203
|
+
isRunning(): boolean {
|
|
204
|
+
return this.child !== null && this.started;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Makes an RPC request
|
|
209
|
+
*/
|
|
210
|
+
async request<T = unknown>(
|
|
211
|
+
method: string,
|
|
212
|
+
params?: Record<string, unknown>,
|
|
213
|
+
opts?: { timeoutMs?: number },
|
|
214
|
+
): Promise<T> {
|
|
215
|
+
if (!this.child || !this.child.stdin) {
|
|
216
|
+
throw new Error("imsg rpc not running");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const id = this.nextId++;
|
|
220
|
+
const payload = {
|
|
221
|
+
jsonrpc: "2.0",
|
|
222
|
+
id,
|
|
223
|
+
method,
|
|
224
|
+
params: params ?? {},
|
|
225
|
+
};
|
|
226
|
+
const line = `${JSON.stringify(payload)}\n`;
|
|
227
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
228
|
+
|
|
229
|
+
const response = new Promise<T>((resolve, reject) => {
|
|
230
|
+
const key = String(id);
|
|
231
|
+
const timer =
|
|
232
|
+
timeoutMs > 0
|
|
233
|
+
? setTimeout(() => {
|
|
234
|
+
this.pending.delete(key);
|
|
235
|
+
reject(new Error(`imsg rpc timeout (${method})`));
|
|
236
|
+
}, timeoutMs)
|
|
237
|
+
: undefined;
|
|
238
|
+
|
|
239
|
+
this.pending.set(key, {
|
|
240
|
+
resolve: (value) => resolve(value as T),
|
|
241
|
+
reject,
|
|
242
|
+
timer,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this.child.stdin.write(line);
|
|
247
|
+
return await response;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handles an incoming line from the RPC process
|
|
252
|
+
*/
|
|
253
|
+
private handleLine(line: string): void {
|
|
254
|
+
let parsed: IMessageRpcResponse<unknown>;
|
|
255
|
+
try {
|
|
256
|
+
parsed = JSON.parse(line) as IMessageRpcResponse<unknown>;
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
259
|
+
this.onError?.(new Error(`imsg rpc: failed to parse ${line}: ${detail}`));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Handle response with ID
|
|
264
|
+
if (parsed.id !== undefined && parsed.id !== null) {
|
|
265
|
+
const key = String(parsed.id);
|
|
266
|
+
const pending = this.pending.get(key);
|
|
267
|
+
if (!pending) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (pending.timer) {
|
|
272
|
+
clearTimeout(pending.timer);
|
|
273
|
+
}
|
|
274
|
+
this.pending.delete(key);
|
|
275
|
+
|
|
276
|
+
if (parsed.error) {
|
|
277
|
+
const baseMessage = parsed.error.message ?? "imsg rpc error";
|
|
278
|
+
const details = parsed.error.data;
|
|
279
|
+
const code = parsed.error.code;
|
|
280
|
+
const suffixes: string[] = [];
|
|
281
|
+
|
|
282
|
+
if (typeof code === "number") {
|
|
283
|
+
suffixes.push(`code=${code}`);
|
|
284
|
+
}
|
|
285
|
+
if (details !== undefined) {
|
|
286
|
+
const detailText =
|
|
287
|
+
typeof details === "string"
|
|
288
|
+
? details
|
|
289
|
+
: JSON.stringify(details, null, 2);
|
|
290
|
+
if (detailText) {
|
|
291
|
+
suffixes.push(detailText);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const msg =
|
|
296
|
+
suffixes.length > 0
|
|
297
|
+
? `${baseMessage}: ${suffixes.join(" ")}`
|
|
298
|
+
: baseMessage;
|
|
299
|
+
pending.reject(new Error(msg));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
pending.resolve(parsed.result);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Handle notification
|
|
308
|
+
if (parsed.method) {
|
|
309
|
+
this.onNotification?.({
|
|
310
|
+
method: parsed.method,
|
|
311
|
+
params: parsed.params,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Fails all pending requests
|
|
318
|
+
*/
|
|
319
|
+
private failAll(err: Error): void {
|
|
320
|
+
for (const [key, pending] of this.pending.entries()) {
|
|
321
|
+
if (pending.timer) {
|
|
322
|
+
clearTimeout(pending.timer);
|
|
323
|
+
}
|
|
324
|
+
pending.reject(err);
|
|
325
|
+
this.pending.delete(key);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Creates and starts an iMessage RPC client
|
|
332
|
+
*/
|
|
333
|
+
export async function createIMessageRpcClient(
|
|
334
|
+
opts: IMessageRpcClientOptions = {},
|
|
335
|
+
): Promise<IMessageRpcClient> {
|
|
336
|
+
const client = new IMessageRpcClient(opts);
|
|
337
|
+
await client.start();
|
|
338
|
+
return client;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* iMessage contact information
|
|
343
|
+
*/
|
|
344
|
+
export interface IMessageContact {
|
|
345
|
+
id: string;
|
|
346
|
+
firstName?: string;
|
|
347
|
+
lastName?: string;
|
|
348
|
+
displayName?: string;
|
|
349
|
+
phones?: string[];
|
|
350
|
+
emails?: string[];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* iMessage chat information
|
|
355
|
+
*/
|
|
356
|
+
export interface IMessageChat {
|
|
357
|
+
id: string;
|
|
358
|
+
chatIdentifier: string;
|
|
359
|
+
displayName?: string;
|
|
360
|
+
isGroup: boolean;
|
|
361
|
+
participants: string[];
|
|
362
|
+
lastMessageDate?: number;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* iMessage message information
|
|
367
|
+
*/
|
|
368
|
+
export interface IMessageMessage {
|
|
369
|
+
id: string;
|
|
370
|
+
chatId: string;
|
|
371
|
+
text?: string;
|
|
372
|
+
sender: string;
|
|
373
|
+
isFromMe: boolean;
|
|
374
|
+
date: number;
|
|
375
|
+
dateRead?: number;
|
|
376
|
+
dateDelivered?: number;
|
|
377
|
+
attachments?: IMessageAttachment[];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* iMessage attachment information
|
|
382
|
+
*/
|
|
383
|
+
export interface IMessageAttachment {
|
|
384
|
+
id: string;
|
|
385
|
+
filename?: string;
|
|
386
|
+
mimeType?: string;
|
|
387
|
+
path?: string;
|
|
388
|
+
size?: number;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Probes the iMessage RPC to check connectivity
|
|
393
|
+
*/
|
|
394
|
+
export async function probeIMessageRpc(params: {
|
|
395
|
+
cliPath?: string;
|
|
396
|
+
dbPath?: string;
|
|
397
|
+
timeoutMs?: number;
|
|
398
|
+
}): Promise<{ ok: boolean; error?: string; version?: string }> {
|
|
399
|
+
const client = new IMessageRpcClient({
|
|
400
|
+
cliPath: params.cliPath,
|
|
401
|
+
dbPath: params.dbPath,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
await client.start();
|
|
406
|
+
const result = await client.request<{ version?: string }>(
|
|
407
|
+
"ping",
|
|
408
|
+
undefined,
|
|
409
|
+
{
|
|
410
|
+
timeoutMs: params.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
await client.stop();
|
|
414
|
+
return { ok: true, version: result?.version };
|
|
415
|
+
} catch (err) {
|
|
416
|
+
await client.stop().catch(() => {});
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
error: err instanceof Error ? err.message : String(err),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Lists all contacts via iMessage RPC
|
|
426
|
+
*/
|
|
427
|
+
export async function listContacts(
|
|
428
|
+
client: IMessageRpcClient,
|
|
429
|
+
): Promise<IMessageContact[]> {
|
|
430
|
+
return client.request<IMessageContact[]>("listContacts");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Lists all chats via iMessage RPC
|
|
435
|
+
*/
|
|
436
|
+
export async function listChats(
|
|
437
|
+
client: IMessageRpcClient,
|
|
438
|
+
): Promise<IMessageChat[]> {
|
|
439
|
+
return client.request<IMessageChat[]>("listChats");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Gets recent messages from a chat
|
|
444
|
+
*/
|
|
445
|
+
export async function getMessages(
|
|
446
|
+
client: IMessageRpcClient,
|
|
447
|
+
params: { chatId: string; limit?: number; before?: number },
|
|
448
|
+
): Promise<IMessageMessage[]> {
|
|
449
|
+
return client.request<IMessageMessage[]>("getMessages", params);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Sends a message via iMessage RPC
|
|
454
|
+
*/
|
|
455
|
+
export async function sendIMessageRpc(
|
|
456
|
+
client: IMessageRpcClient,
|
|
457
|
+
params: {
|
|
458
|
+
to: string;
|
|
459
|
+
text: string;
|
|
460
|
+
attachments?: string[];
|
|
461
|
+
service?: "iMessage" | "SMS";
|
|
462
|
+
},
|
|
463
|
+
): Promise<{ messageId: string }> {
|
|
464
|
+
return client.request<{ messageId: string }>("send", params);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Gets chat info via iMessage RPC
|
|
469
|
+
*/
|
|
470
|
+
export async function getChatInfo(
|
|
471
|
+
client: IMessageRpcClient,
|
|
472
|
+
params: { chatId: string },
|
|
473
|
+
): Promise<IMessageChat | null> {
|
|
474
|
+
return client.request<IMessageChat | null>("getChatInfo", params);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Gets contact info via iMessage RPC
|
|
479
|
+
*/
|
|
480
|
+
export async function getContactInfo(
|
|
481
|
+
client: IMessageRpcClient,
|
|
482
|
+
params: { identifier: string },
|
|
483
|
+
): Promise<IMessageContact | null> {
|
|
484
|
+
return client.request<IMessageContact | null>("getContactInfo", params);
|
|
485
|
+
}
|