chat-adapter-imessage 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +720 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -5
- package/index.js +0 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Photon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# chat-adapter-imessage
|
|
2
|
+
|
|
3
|
+
iMessage community adapter for [Chat SDK](https://chat-sdk.dev/docs). Supports both local (on-device) and remote ([Photon](https://photon.codes)-based) iMessage integration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add chat chat-adapter-imessage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
The adapter supports two modes: **local** (running directly on a Mac with iMessage) and **remote** (connecting to a [Photon](https://photon.codes) iMessage server). The mode is auto-detected from the `IMESSAGE_LOCAL` environment variable.
|
|
14
|
+
|
|
15
|
+
### Remote mode
|
|
16
|
+
|
|
17
|
+
Recommended for production. Connects to [Photon](https://photon.codes)'s managed iMessage service over HTTP and Socket.IO, so your bot can run on any platform.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Chat } from "chat";
|
|
21
|
+
import { createiMessageAdapter } from "chat-adapter-imessage";
|
|
22
|
+
|
|
23
|
+
const bot = new Chat({
|
|
24
|
+
userName: "mybot",
|
|
25
|
+
adapters: {
|
|
26
|
+
imessage: createiMessageAdapter({
|
|
27
|
+
local: false,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
bot.onNewMention(async (thread, message) => {
|
|
33
|
+
await thread.post("Hello from iMessage!");
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Local mode
|
|
38
|
+
|
|
39
|
+
For development or self-hosted deployments using [imessage-kit](https://github.com/photon-hq/imessage-kit). Reads from the local iMessage database and sends via AppleScript. Must run on macOS with **Full Disk Access** granted.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { Chat } from "chat";
|
|
43
|
+
import { createiMessageAdapter } from "chat-adapter-imessage";
|
|
44
|
+
|
|
45
|
+
const bot = new Chat({
|
|
46
|
+
userName: "mybot",
|
|
47
|
+
adapters: {
|
|
48
|
+
imessage: createiMessageAdapter({
|
|
49
|
+
local: true,
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
bot.onNewMention(async (thread, message) => {
|
|
55
|
+
await thread.post("Hello from iMessage!");
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Setup
|
|
60
|
+
|
|
61
|
+
### Remote mode
|
|
62
|
+
|
|
63
|
+
Remote mode connects to a [Photon](https://photon.codes) iMessage server, which handles the macOS-side integration on your behalf. You'll need an active Photon subscription to get your server credentials.
|
|
64
|
+
|
|
65
|
+
1. [Request access](https://photon.codes) from Photon to get your server credentials
|
|
66
|
+
2. Copy your **server URL** and **API key** from the Photon dashboard
|
|
67
|
+
3. Set `IMESSAGE_SERVER_URL` and `IMESSAGE_API_KEY` environment variables
|
|
68
|
+
4. Set `IMESSAGE_LOCAL=false`
|
|
69
|
+
|
|
70
|
+
### Local mode
|
|
71
|
+
|
|
72
|
+
Local mode requires the adapter to run directly on a macOS machine with iMessage. It uses Apple's native APIs — reading from the local `chat.db` database and sending messages via AppleScript — with no external server required.
|
|
73
|
+
|
|
74
|
+
1. Grant **Full Disk Access** to your terminal or application in **System Settings > Privacy & Security > Full Disk Access**
|
|
75
|
+
2. Ensure iMessage is signed in and working on the Mac
|
|
76
|
+
3. No additional environment variables are required — local mode is the default
|
|
77
|
+
|
|
78
|
+
## Receiving messages
|
|
79
|
+
|
|
80
|
+
Call `startGatewayListener()` to listen for new messages in real-time. In remote mode, this uses Socket.IO push events. In local mode, it polls the iMessage database.
|
|
81
|
+
|
|
82
|
+
In serverless environments, use a cron job to maintain the connection.
|
|
83
|
+
|
|
84
|
+
## Gateway setup for serverless
|
|
85
|
+
|
|
86
|
+
### 1. Create Gateway route
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// app/api/imessage/gateway/route.ts
|
|
90
|
+
import { after } from "next/server";
|
|
91
|
+
import { bot } from "@/lib/bot";
|
|
92
|
+
|
|
93
|
+
export const maxDuration = 800;
|
|
94
|
+
|
|
95
|
+
export async function GET(request: Request): Promise<Response> {
|
|
96
|
+
const cronSecret = process.env.CRON_SECRET;
|
|
97
|
+
if (!cronSecret) {
|
|
98
|
+
return new Response("CRON_SECRET not configured", { status: 500 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const authHeader = request.headers.get("authorization");
|
|
102
|
+
if (authHeader !== `Bearer ${cronSecret}`) {
|
|
103
|
+
return new Response("Unauthorized", { status: 401 });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const durationMs = 600 * 1000;
|
|
107
|
+
|
|
108
|
+
return bot.adapters.imessage.startGatewayListener(
|
|
109
|
+
{ waitUntil: (task) => after(() => task) },
|
|
110
|
+
durationMs
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 2. Configure Vercel Cron
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
// vercel.json
|
|
119
|
+
{
|
|
120
|
+
"crons": [
|
|
121
|
+
{
|
|
122
|
+
"path": "/api/imessage/gateway",
|
|
123
|
+
"schedule": "*/9 * * * *"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This runs every 9 minutes, ensuring overlap with the 10-minute listener duration.
|
|
130
|
+
|
|
131
|
+
### 3. Environment variables
|
|
132
|
+
|
|
133
|
+
`CRON_SECRET` is automatically added by Vercel when you configure cron jobs.
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
| Option | Required | Description |
|
|
138
|
+
|--------|----------|-------------|
|
|
139
|
+
| `local` | No | `true` for local mode, `false` for remote. Auto-detected from `IMESSAGE_LOCAL` (default: `true`) |
|
|
140
|
+
| `serverUrl` | Remote only | URL of the remote iMessage server. Auto-detected from `IMESSAGE_SERVER_URL` |
|
|
141
|
+
| `apiKey` | Remote only | API key for remote server authentication. Auto-detected from `IMESSAGE_API_KEY` |
|
|
142
|
+
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
|
|
143
|
+
|
|
144
|
+
## Environment variables
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# .env.local
|
|
148
|
+
IMESSAGE_LOCAL=false # Set to "false" for remote mode (default: true)
|
|
149
|
+
IMESSAGE_SERVER_URL=https://... # Required for remote mode
|
|
150
|
+
IMESSAGE_API_KEY=... # Required for remote mode
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Features
|
|
154
|
+
|
|
155
|
+
| Feature | Supported |
|
|
156
|
+
|---------|-----------|
|
|
157
|
+
| Mentions | DMs only |
|
|
158
|
+
| Reactions (add/remove) | Remote only |
|
|
159
|
+
| Modals | Limited (Remote only) |
|
|
160
|
+
| Cards | No |
|
|
161
|
+
| Streaming | No |
|
|
162
|
+
| DMs | Yes |
|
|
163
|
+
| Ephemeral messages | No |
|
|
164
|
+
| File uploads | Yes |
|
|
165
|
+
| Typing indicator | Remote only |
|
|
166
|
+
| Message history | Yes |
|
|
167
|
+
| Message editing | Remote only |
|
|
168
|
+
|
|
169
|
+
## Modals (Limited)
|
|
170
|
+
|
|
171
|
+
Remote mode supports limited modal functionality by mapping the Chat SDK's `openModal()` to iMessage native polls via the Photon SDK. Only `Select` children are supported — the first `Select` in the modal is used to create a poll.
|
|
172
|
+
|
|
173
|
+
- `Modal.title` becomes the poll question
|
|
174
|
+
- `Select.options` become the poll choices
|
|
175
|
+
- Votes trigger `onModalSubmit` with the selected option's `value`
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { Chat, Modal, Select, SelectOption } from "chat";
|
|
179
|
+
import { createiMessageAdapter } from "chat-adapter-imessage";
|
|
180
|
+
|
|
181
|
+
const bot = new Chat({
|
|
182
|
+
userName: "mybot",
|
|
183
|
+
adapters: {
|
|
184
|
+
imessage: createiMessageAdapter({ local: false }),
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
bot.onNewMention(async (thread, message) => {
|
|
189
|
+
await message.openModal(
|
|
190
|
+
Modal({
|
|
191
|
+
callbackId: "fav-color",
|
|
192
|
+
title: "What is your favorite color?",
|
|
193
|
+
children: [
|
|
194
|
+
Select({
|
|
195
|
+
id: "color",
|
|
196
|
+
label: "Pick a color",
|
|
197
|
+
options: [
|
|
198
|
+
SelectOption({ label: "Red", value: "red" }),
|
|
199
|
+
SelectOption({ label: "Blue", value: "blue" }),
|
|
200
|
+
SelectOption({ label: "Green", value: "green" }),
|
|
201
|
+
],
|
|
202
|
+
}),
|
|
203
|
+
],
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
bot.onModalSubmit("fav-color", async (event) => {
|
|
209
|
+
const color = event.values.color;
|
|
210
|
+
// color will be "red", "blue", or "green"
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Not supported:**
|
|
215
|
+
|
|
216
|
+
- `Select.placeholder` and `Select.label` — iMessage polls don't have these fields
|
|
217
|
+
- `TextInput`, `RadioSelect`, and other modal children — silently ignored
|
|
218
|
+
- `Modal.submitLabel` and `Modal.closeLabel` — not applicable to polls
|
|
219
|
+
- Only the **first** `Select` child is used; other children are ignored
|
|
220
|
+
- Local mode — `openModal()` throws `NotImplementedError`
|
|
221
|
+
|
|
222
|
+
## Tapback reactions
|
|
223
|
+
|
|
224
|
+
iMessage uses tapbacks instead of emoji reactions. The adapter maps standard emoji names to iMessage tapbacks:
|
|
225
|
+
|
|
226
|
+
| Emoji | Tapback |
|
|
227
|
+
|-------|---------|
|
|
228
|
+
| `love` / `heart` | Love |
|
|
229
|
+
| `like` / `thumbs_up` | Like |
|
|
230
|
+
| `dislike` / `thumbs_down` | Dislike |
|
|
231
|
+
| `laugh` | Laugh |
|
|
232
|
+
| `emphasize` / `exclamation` | Emphasize |
|
|
233
|
+
| `question` | Question |
|
|
234
|
+
|
|
235
|
+
## Limitations
|
|
236
|
+
|
|
237
|
+
- **Local mode**: Only supports sending/receiving messages, message history, and file uploads. Reactions, typing indicators, message editing, modals, and thread fetching require remote mode.
|
|
238
|
+
- **Formatting**: iMessage is plain-text only. Markdown formatting (bold, italic, etc.) is stripped when sending messages, preserving only the text content.
|
|
239
|
+
- **Platform**: Local mode requires macOS. Remote mode can run on any platform — [Photon](https://photon.codes) manages the iMessage infrastructure for you.
|
|
240
|
+
- **Cards**: iMessage has no support for structured card layouts.
|
|
241
|
+
- **Modals**: Limited to `Select`-based modals mapped to iMessage native polls. Only the first `Select` child is used; `placeholder`, `label`, `TextInput`, `RadioSelect`, and other fields are not supported. Remote mode only.
|
|
242
|
+
|
|
243
|
+
## Troubleshooting
|
|
244
|
+
|
|
245
|
+
### "serverUrl is required" error
|
|
246
|
+
|
|
247
|
+
- Set `IMESSAGE_SERVER_URL` or pass `serverUrl` in config when using remote mode
|
|
248
|
+
- This error occurs when `IMESSAGE_LOCAL=false` but no server URL is provided
|
|
249
|
+
|
|
250
|
+
### "apiKey is required" error
|
|
251
|
+
|
|
252
|
+
- Set `IMESSAGE_API_KEY` or pass `apiKey` in config when using remote mode
|
|
253
|
+
|
|
254
|
+
### Local mode not receiving messages
|
|
255
|
+
|
|
256
|
+
- Verify **Full Disk Access** is granted to your terminal or application
|
|
257
|
+
- Check that iMessage is signed in and working
|
|
258
|
+
- Messages are polled from the local database — there may be a short delay
|
|
259
|
+
|
|
260
|
+
### Remote mode connection issues
|
|
261
|
+
|
|
262
|
+
- Verify the server URL is correct and accessible
|
|
263
|
+
- Check that the API key matches your Photon iMessage service credentials
|
|
264
|
+
- Confirm your Photon subscription is active
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { AdvancedIMessageKit } from '@photon-ai/advanced-imessage-kit';
|
|
2
|
+
import { IMessageSDK } from '@photon-ai/imessage-kit';
|
|
3
|
+
import { BaseFormatConverter, Root, Logger, Adapter, ChatInstance, WebhookOptions, AdapterPostableMessage, RawMessage, Message, FetchOptions, FetchResult, ThreadInfo, EmojiValue, ModalElement, FormattedContent } from 'chat';
|
|
4
|
+
|
|
5
|
+
/** Thread ID components for iMessage */
|
|
6
|
+
interface iMessageThreadId {
|
|
7
|
+
/** Chat GUID (e.g., "iMessage;-;+1234567890") */
|
|
8
|
+
chatGuid: string;
|
|
9
|
+
}
|
|
10
|
+
/** Normalized message data from either local or remote SDK */
|
|
11
|
+
interface iMessageGatewayMessageData {
|
|
12
|
+
/** Attachments */
|
|
13
|
+
attachments: iMessageAttachment[];
|
|
14
|
+
/** Chat GUID this message belongs to */
|
|
15
|
+
chatId: string;
|
|
16
|
+
/** Message timestamp (ISO string) */
|
|
17
|
+
date: string;
|
|
18
|
+
/** Message GUID */
|
|
19
|
+
guid: string;
|
|
20
|
+
/** Whether the message is from the current user */
|
|
21
|
+
isFromMe: boolean;
|
|
22
|
+
/** Whether this is a group chat */
|
|
23
|
+
isGroupChat: boolean;
|
|
24
|
+
/** Raw data from the SDK */
|
|
25
|
+
raw?: unknown;
|
|
26
|
+
/** Sender identifier (phone/email) */
|
|
27
|
+
sender: string;
|
|
28
|
+
/** Sender display name */
|
|
29
|
+
senderName: string | null;
|
|
30
|
+
/** Source SDK */
|
|
31
|
+
source: "local" | "remote";
|
|
32
|
+
/** Message text content */
|
|
33
|
+
text: string | null;
|
|
34
|
+
}
|
|
35
|
+
interface iMessageAttachment {
|
|
36
|
+
filename: string;
|
|
37
|
+
id: string;
|
|
38
|
+
mimeType: string;
|
|
39
|
+
size: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Payload shape from imessage-kit's native webhook.
|
|
43
|
+
* The SDK POSTs the Message object directly when webhook config is set.
|
|
44
|
+
*/
|
|
45
|
+
interface NativeWebhookPayload {
|
|
46
|
+
attachments: Array<{
|
|
47
|
+
createdAt: string;
|
|
48
|
+
filename: string;
|
|
49
|
+
id: string;
|
|
50
|
+
isImage: boolean;
|
|
51
|
+
mimeType: string;
|
|
52
|
+
path: string;
|
|
53
|
+
size: number;
|
|
54
|
+
}>;
|
|
55
|
+
chatId: string;
|
|
56
|
+
date: string;
|
|
57
|
+
guid: string;
|
|
58
|
+
isFromMe: boolean;
|
|
59
|
+
isGroupChat: boolean;
|
|
60
|
+
isReaction: boolean;
|
|
61
|
+
sender: string;
|
|
62
|
+
senderName: string | null;
|
|
63
|
+
service: string;
|
|
64
|
+
text: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* iMessage format conversion using AST-based parsing.
|
|
69
|
+
*
|
|
70
|
+
* iMessage supports plain text only -- no rich formatting syntax.
|
|
71
|
+
* The converter strips formatting markers and outputs clean plain text,
|
|
72
|
+
* preserving structure (lists, blockquotes, code blocks) with whitespace.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
declare class iMessageFormatConverter extends BaseFormatConverter {
|
|
76
|
+
/**
|
|
77
|
+
* Render an AST to iMessage plain text format.
|
|
78
|
+
* Strips all formatting markers since iMessage doesn't support rich text via API.
|
|
79
|
+
*/
|
|
80
|
+
fromAst(ast: Root): string;
|
|
81
|
+
/**
|
|
82
|
+
* Parse iMessage text into an AST.
|
|
83
|
+
* iMessage sends plain text, so we just parse it as markdown.
|
|
84
|
+
*/
|
|
85
|
+
toAst(text: string): Root;
|
|
86
|
+
private nodeToPlainText;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface iMessageAdapterLocalConfig {
|
|
90
|
+
apiKey?: string;
|
|
91
|
+
local: true;
|
|
92
|
+
logger: Logger;
|
|
93
|
+
serverUrl?: string;
|
|
94
|
+
}
|
|
95
|
+
interface iMessageAdapterRemoteConfig {
|
|
96
|
+
apiKey: string;
|
|
97
|
+
local: false;
|
|
98
|
+
logger: Logger;
|
|
99
|
+
serverUrl: string;
|
|
100
|
+
}
|
|
101
|
+
type iMessageAdapterConfig = iMessageAdapterLocalConfig | iMessageAdapterRemoteConfig;
|
|
102
|
+
declare class iMessageAdapter implements Adapter {
|
|
103
|
+
readonly name = "imessage";
|
|
104
|
+
readonly userName: string;
|
|
105
|
+
readonly local: boolean;
|
|
106
|
+
readonly serverUrl?: string;
|
|
107
|
+
readonly apiKey?: string;
|
|
108
|
+
readonly sdk: IMessageSDK | AdvancedIMessageKit;
|
|
109
|
+
private chat;
|
|
110
|
+
private readonly logger;
|
|
111
|
+
private readonly formatConverter;
|
|
112
|
+
private readonly modalPollMap;
|
|
113
|
+
constructor(config: iMessageAdapterConfig);
|
|
114
|
+
initialize(chat: ChatInstance): Promise<void>;
|
|
115
|
+
handleWebhook(_request: Request, _options?: WebhookOptions): Promise<Response>;
|
|
116
|
+
postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage>;
|
|
117
|
+
editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage>;
|
|
118
|
+
deleteMessage(_threadId: string, _messageId: string): Promise<void>;
|
|
119
|
+
parseMessage(raw: unknown): Message;
|
|
120
|
+
fetchMessages(threadId: string, options?: FetchOptions): Promise<FetchResult>;
|
|
121
|
+
fetchThread(threadId: string): Promise<ThreadInfo>;
|
|
122
|
+
addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
|
|
123
|
+
removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
|
|
124
|
+
startTyping(threadId: string, _status?: string): Promise<void>;
|
|
125
|
+
openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
|
|
126
|
+
viewId: string;
|
|
127
|
+
}>;
|
|
128
|
+
renderFormatted(content: FormattedContent): string;
|
|
129
|
+
encodeThreadId(platformData: iMessageThreadId): string;
|
|
130
|
+
decodeThreadId(threadId: string): iMessageThreadId;
|
|
131
|
+
isDM(threadId: string): boolean;
|
|
132
|
+
startGatewayListener(options: WebhookOptions, durationMs?: number, abortSignal?: AbortSignal): Promise<Response>;
|
|
133
|
+
private runGatewayListener;
|
|
134
|
+
private handlePollVoteAsModalSubmit;
|
|
135
|
+
private writeTempFiles;
|
|
136
|
+
private fetchMessagesLocal;
|
|
137
|
+
private fetchMessagesRemote;
|
|
138
|
+
private normalizeLocalMessage;
|
|
139
|
+
private normalizeRemoteMessage;
|
|
140
|
+
private buildMessage;
|
|
141
|
+
private handleGatewayMessage;
|
|
142
|
+
private emojiToTapback;
|
|
143
|
+
private getAttachmentType;
|
|
144
|
+
}
|
|
145
|
+
declare function createiMessageAdapter(config?: Partial<iMessageAdapterConfig>): iMessageAdapter;
|
|
146
|
+
|
|
147
|
+
export { type NativeWebhookPayload, createiMessageAdapter, iMessageAdapter, type iMessageAdapterConfig, type iMessageAdapterLocalConfig, type iMessageAdapterRemoteConfig, iMessageFormatConverter, type iMessageGatewayMessageData, type iMessageThreadId };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { extractFiles, ValidationError } from "@chat-adapter/shared";
|
|
6
|
+
import {
|
|
7
|
+
AdvancedIMessageKit,
|
|
8
|
+
isPollVote,
|
|
9
|
+
parsePollVotes
|
|
10
|
+
} from "@photon-ai/advanced-imessage-kit";
|
|
11
|
+
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
12
|
+
import { ConsoleLogger, Message, NotImplementedError, parseMarkdown as parseMarkdown2 } from "chat";
|
|
13
|
+
|
|
14
|
+
// src/markdown.ts
|
|
15
|
+
import {
|
|
16
|
+
BaseFormatConverter,
|
|
17
|
+
getNodeChildren,
|
|
18
|
+
getNodeValue,
|
|
19
|
+
isBlockquoteNode,
|
|
20
|
+
isCodeNode,
|
|
21
|
+
isDeleteNode,
|
|
22
|
+
isEmphasisNode,
|
|
23
|
+
isInlineCodeNode,
|
|
24
|
+
isLinkNode,
|
|
25
|
+
isListItemNode,
|
|
26
|
+
isListNode,
|
|
27
|
+
isParagraphNode,
|
|
28
|
+
isStrongNode,
|
|
29
|
+
isTextNode,
|
|
30
|
+
parseMarkdown
|
|
31
|
+
} from "chat";
|
|
32
|
+
var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
33
|
+
/**
|
|
34
|
+
* Render an AST to iMessage plain text format.
|
|
35
|
+
* Strips all formatting markers since iMessage doesn't support rich text via API.
|
|
36
|
+
*/
|
|
37
|
+
fromAst(ast) {
|
|
38
|
+
return this.fromAstWithNodeConverter(
|
|
39
|
+
ast,
|
|
40
|
+
(node) => this.nodeToPlainText(node)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse iMessage text into an AST.
|
|
45
|
+
* iMessage sends plain text, so we just parse it as markdown.
|
|
46
|
+
*/
|
|
47
|
+
toAst(text) {
|
|
48
|
+
return parseMarkdown(text);
|
|
49
|
+
}
|
|
50
|
+
nodeToPlainText(node) {
|
|
51
|
+
if (isParagraphNode(node)) {
|
|
52
|
+
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
53
|
+
}
|
|
54
|
+
if (isTextNode(node)) {
|
|
55
|
+
return node.value;
|
|
56
|
+
}
|
|
57
|
+
if (isStrongNode(node) || isEmphasisNode(node) || isDeleteNode(node)) {
|
|
58
|
+
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
59
|
+
}
|
|
60
|
+
if (isInlineCodeNode(node)) {
|
|
61
|
+
return node.value;
|
|
62
|
+
}
|
|
63
|
+
if (isCodeNode(node)) {
|
|
64
|
+
return node.value;
|
|
65
|
+
}
|
|
66
|
+
if (isLinkNode(node)) {
|
|
67
|
+
const linkText = getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
68
|
+
return linkText ? `${linkText} (${node.url})` : node.url;
|
|
69
|
+
}
|
|
70
|
+
if (isBlockquoteNode(node)) {
|
|
71
|
+
return getNodeChildren(node).map((child) => `> ${this.nodeToPlainText(child)}`).join("\n");
|
|
72
|
+
}
|
|
73
|
+
if (isListNode(node)) {
|
|
74
|
+
return getNodeChildren(node).map((item, i) => {
|
|
75
|
+
const prefix = node.ordered ? `${i + 1}.` : "-";
|
|
76
|
+
const content = getNodeChildren(item).map((child) => this.nodeToPlainText(child)).join("");
|
|
77
|
+
return `${prefix} ${content}`;
|
|
78
|
+
}).join("\n");
|
|
79
|
+
}
|
|
80
|
+
if (isListItemNode(node)) {
|
|
81
|
+
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
82
|
+
}
|
|
83
|
+
if (node.type === "break") {
|
|
84
|
+
return "\n";
|
|
85
|
+
}
|
|
86
|
+
if (node.type === "thematicBreak") {
|
|
87
|
+
return "---";
|
|
88
|
+
}
|
|
89
|
+
const children = getNodeChildren(node);
|
|
90
|
+
if (children.length > 0) {
|
|
91
|
+
return children.map((child) => this.nodeToPlainText(child)).join("");
|
|
92
|
+
}
|
|
93
|
+
return getNodeValue(node);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/index.ts
|
|
98
|
+
var iMessageAdapter = class {
|
|
99
|
+
name = "imessage";
|
|
100
|
+
userName = "";
|
|
101
|
+
local;
|
|
102
|
+
serverUrl;
|
|
103
|
+
apiKey;
|
|
104
|
+
sdk;
|
|
105
|
+
chat = null;
|
|
106
|
+
logger;
|
|
107
|
+
formatConverter = new iMessageFormatConverter();
|
|
108
|
+
modalPollMap = /* @__PURE__ */ new Map();
|
|
109
|
+
constructor(config) {
|
|
110
|
+
if (config.local && process.platform !== "darwin") {
|
|
111
|
+
throw new ValidationError(
|
|
112
|
+
"imessage",
|
|
113
|
+
"iMessage adapter local mode requires macOS. Current platform: " + process.platform
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
this.local = config.local;
|
|
117
|
+
this.serverUrl = config.serverUrl;
|
|
118
|
+
this.apiKey = config.apiKey;
|
|
119
|
+
this.logger = config.logger;
|
|
120
|
+
if (config.local) {
|
|
121
|
+
this.sdk = new IMessageSDK();
|
|
122
|
+
} else {
|
|
123
|
+
this.sdk = AdvancedIMessageKit.getInstance({
|
|
124
|
+
serverUrl: config.serverUrl,
|
|
125
|
+
apiKey: config.apiKey
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async initialize(chat) {
|
|
130
|
+
this.chat = chat;
|
|
131
|
+
this.logger.info("iMessage adapter initialized", {
|
|
132
|
+
local: this.local,
|
|
133
|
+
serverUrl: this.serverUrl ? "configured" : "not configured"
|
|
134
|
+
});
|
|
135
|
+
if (!this.local) {
|
|
136
|
+
const sdk = this.sdk;
|
|
137
|
+
await sdk.connect();
|
|
138
|
+
await new Promise((resolve) => sdk.once("ready", resolve));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async handleWebhook(_request, _options) {
|
|
142
|
+
return new Response("Webhook not supported -- use startGatewayListener()", {
|
|
143
|
+
status: 501
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async postMessage(threadId, message) {
|
|
147
|
+
const { chatGuid } = this.decodeThreadId(threadId);
|
|
148
|
+
const text = this.formatConverter.renderPostable(message);
|
|
149
|
+
const files = extractFiles(message);
|
|
150
|
+
const tempFiles = files.length > 0 ? await this.writeTempFiles(files) : null;
|
|
151
|
+
try {
|
|
152
|
+
if (this.local) {
|
|
153
|
+
const sdk2 = this.sdk;
|
|
154
|
+
const recipient = chatGuid.split(";").pop() ?? chatGuid;
|
|
155
|
+
const content = tempFiles?.paths.length ? { text: text || void 0, files: tempFiles.paths } : text;
|
|
156
|
+
const result2 = await sdk2.send(recipient, content);
|
|
157
|
+
return {
|
|
158
|
+
id: result2.message?.guid ?? `local-${Date.now()}`,
|
|
159
|
+
threadId,
|
|
160
|
+
raw: result2
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const sdk = this.sdk;
|
|
164
|
+
let result;
|
|
165
|
+
if (text || !tempFiles) {
|
|
166
|
+
result = await sdk.messages.sendMessage({
|
|
167
|
+
chatGuid,
|
|
168
|
+
message: text
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (tempFiles) {
|
|
172
|
+
for (const filePath of tempFiles.paths) {
|
|
173
|
+
const attachmentResult = await sdk.attachments.sendAttachment({
|
|
174
|
+
chatGuid,
|
|
175
|
+
filePath
|
|
176
|
+
});
|
|
177
|
+
result ??= attachmentResult;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
id: result?.guid ?? `msg-${Date.now()}`,
|
|
182
|
+
threadId,
|
|
183
|
+
raw: result
|
|
184
|
+
};
|
|
185
|
+
} finally {
|
|
186
|
+
if (tempFiles) {
|
|
187
|
+
await rm(tempFiles.dir, { recursive: true }).catch(() => {
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async editMessage(threadId, messageId, message) {
|
|
193
|
+
if (this.local) {
|
|
194
|
+
throw new NotImplementedError(
|
|
195
|
+
"editMessage is not supported in local mode",
|
|
196
|
+
"editMessage"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const text = this.formatConverter.renderPostable(message);
|
|
200
|
+
const sdk = this.sdk;
|
|
201
|
+
const result = await sdk.messages.editMessage({
|
|
202
|
+
messageGuid: messageId,
|
|
203
|
+
editedMessage: text,
|
|
204
|
+
backwardsCompatibilityMessage: text
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
id: result.guid,
|
|
208
|
+
threadId,
|
|
209
|
+
raw: result
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async deleteMessage(_threadId, _messageId) {
|
|
213
|
+
throw new NotImplementedError(
|
|
214
|
+
"deleteMessage is not implemented",
|
|
215
|
+
"deleteMessage"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
parseMessage(raw) {
|
|
219
|
+
const data = this.local ? this.normalizeLocalMessage(raw) : this.normalizeRemoteMessage(raw);
|
|
220
|
+
return this.buildMessage(data);
|
|
221
|
+
}
|
|
222
|
+
async fetchMessages(threadId, options) {
|
|
223
|
+
const { chatGuid } = this.decodeThreadId(threadId);
|
|
224
|
+
const direction = options?.direction ?? "backward";
|
|
225
|
+
const limit = options?.limit ?? 50;
|
|
226
|
+
const cursor = options?.cursor;
|
|
227
|
+
if (this.local) {
|
|
228
|
+
return this.fetchMessagesLocal(chatGuid, direction, limit, cursor);
|
|
229
|
+
}
|
|
230
|
+
return this.fetchMessagesRemote(chatGuid, direction, limit, cursor);
|
|
231
|
+
}
|
|
232
|
+
async fetchThread(threadId) {
|
|
233
|
+
if (this.local) {
|
|
234
|
+
throw new NotImplementedError(
|
|
235
|
+
"fetchThread is not supported in local mode",
|
|
236
|
+
"fetchThread"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const { chatGuid } = this.decodeThreadId(threadId);
|
|
240
|
+
const sdk = this.sdk;
|
|
241
|
+
const chat = await sdk.chats.getChat(chatGuid);
|
|
242
|
+
const isDM = chatGuid.includes(";-;");
|
|
243
|
+
return {
|
|
244
|
+
id: threadId,
|
|
245
|
+
channelId: chatGuid,
|
|
246
|
+
channelName: chat.displayName || void 0,
|
|
247
|
+
isDM,
|
|
248
|
+
metadata: {
|
|
249
|
+
chatIdentifier: chat.chatIdentifier,
|
|
250
|
+
style: chat.style,
|
|
251
|
+
participants: chat.participants,
|
|
252
|
+
isArchived: chat.isArchived,
|
|
253
|
+
raw: chat
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async addReaction(threadId, messageId, emoji) {
|
|
258
|
+
if (this.local) {
|
|
259
|
+
throw new NotImplementedError(
|
|
260
|
+
"addReaction is not supported in local mode",
|
|
261
|
+
"addReaction"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const tapback = this.emojiToTapback(emoji);
|
|
265
|
+
const { chatGuid } = this.decodeThreadId(threadId);
|
|
266
|
+
const sdk = this.sdk;
|
|
267
|
+
await sdk.messages.sendReaction({
|
|
268
|
+
chatGuid,
|
|
269
|
+
messageGuid: messageId,
|
|
270
|
+
reaction: tapback
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async removeReaction(threadId, messageId, emoji) {
|
|
274
|
+
if (this.local) {
|
|
275
|
+
throw new NotImplementedError(
|
|
276
|
+
"removeReaction is not supported in local mode",
|
|
277
|
+
"removeReaction"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const tapback = this.emojiToTapback(emoji);
|
|
281
|
+
const { chatGuid } = this.decodeThreadId(threadId);
|
|
282
|
+
const sdk = this.sdk;
|
|
283
|
+
await sdk.messages.sendReaction({
|
|
284
|
+
chatGuid,
|
|
285
|
+
messageGuid: messageId,
|
|
286
|
+
reaction: `-${tapback}`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async startTyping(threadId, _status) {
|
|
290
|
+
if (this.local) {
|
|
291
|
+
throw new NotImplementedError(
|
|
292
|
+
"startTyping is not supported in local mode",
|
|
293
|
+
"startTyping"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const { chatGuid } = this.decodeThreadId(threadId);
|
|
297
|
+
const sdk = this.sdk;
|
|
298
|
+
await sdk.chats.startTyping(chatGuid);
|
|
299
|
+
setTimeout(() => sdk.chats.stopTyping(chatGuid), 3e3);
|
|
300
|
+
}
|
|
301
|
+
async openModal(triggerId, modal, contextId) {
|
|
302
|
+
if (this.local) {
|
|
303
|
+
throw new NotImplementedError(
|
|
304
|
+
"openModal is not supported in local mode",
|
|
305
|
+
"openModal"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const select = modal.children.find(
|
|
309
|
+
(c) => c.type === "select"
|
|
310
|
+
);
|
|
311
|
+
if (!select) {
|
|
312
|
+
throw new ValidationError(
|
|
313
|
+
"imessage",
|
|
314
|
+
"openModal requires at least one Select child \u2014 iMessage modals map to native polls"
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const { chatGuid } = this.decodeThreadId(triggerId);
|
|
318
|
+
const sdk = this.sdk;
|
|
319
|
+
const result = await sdk.polls.create({
|
|
320
|
+
chatGuid,
|
|
321
|
+
title: modal.title,
|
|
322
|
+
options: select.options.map((o) => o.label)
|
|
323
|
+
});
|
|
324
|
+
this.modalPollMap.set(result.guid, {
|
|
325
|
+
callbackId: modal.callbackId,
|
|
326
|
+
selectId: select.id,
|
|
327
|
+
options: select.options,
|
|
328
|
+
contextId,
|
|
329
|
+
privateMetadata: modal.privateMetadata
|
|
330
|
+
});
|
|
331
|
+
return { viewId: result.guid };
|
|
332
|
+
}
|
|
333
|
+
renderFormatted(content) {
|
|
334
|
+
return this.formatConverter.fromAst(content);
|
|
335
|
+
}
|
|
336
|
+
encodeThreadId(platformData) {
|
|
337
|
+
return `imessage:${platformData.chatGuid}`;
|
|
338
|
+
}
|
|
339
|
+
decodeThreadId(threadId) {
|
|
340
|
+
if (!threadId.startsWith("imessage:")) {
|
|
341
|
+
throw new ValidationError(
|
|
342
|
+
"imessage",
|
|
343
|
+
`Invalid iMessage thread ID: ${threadId}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return { chatGuid: threadId.slice("imessage:".length) };
|
|
347
|
+
}
|
|
348
|
+
isDM(threadId) {
|
|
349
|
+
const { chatGuid } = this.decodeThreadId(threadId);
|
|
350
|
+
return chatGuid.includes(";-;");
|
|
351
|
+
}
|
|
352
|
+
async startGatewayListener(options, durationMs = 18e4, abortSignal) {
|
|
353
|
+
if (!this.chat) {
|
|
354
|
+
return new Response("Chat instance not initialized", { status: 500 });
|
|
355
|
+
}
|
|
356
|
+
if (!options.waitUntil) {
|
|
357
|
+
return new Response("waitUntil not provided", { status: 500 });
|
|
358
|
+
}
|
|
359
|
+
this.logger.info("Starting iMessage Gateway listener", {
|
|
360
|
+
durationMs,
|
|
361
|
+
mode: this.local ? "local" : "remote"
|
|
362
|
+
});
|
|
363
|
+
const listenerPromise = this.runGatewayListener(durationMs, abortSignal, options);
|
|
364
|
+
options.waitUntil(listenerPromise);
|
|
365
|
+
return new Response(
|
|
366
|
+
JSON.stringify({
|
|
367
|
+
status: "listening",
|
|
368
|
+
durationMs,
|
|
369
|
+
mode: this.local ? "local" : "remote",
|
|
370
|
+
message: `Gateway listener started, will run for ${durationMs / 1e3} seconds`
|
|
371
|
+
}),
|
|
372
|
+
{
|
|
373
|
+
status: 200,
|
|
374
|
+
headers: { "Content-Type": "application/json" }
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
async runGatewayListener(durationMs, abortSignal, options) {
|
|
379
|
+
let isShuttingDown = false;
|
|
380
|
+
let remoteGatewaySdk = null;
|
|
381
|
+
try {
|
|
382
|
+
if (this.local) {
|
|
383
|
+
const sdk = this.sdk;
|
|
384
|
+
await sdk.startWatching({
|
|
385
|
+
onMessage: async (message) => {
|
|
386
|
+
if (isShuttingDown) return;
|
|
387
|
+
if (message.isFromMe) return;
|
|
388
|
+
const data = this.normalizeLocalMessage(message);
|
|
389
|
+
this.handleGatewayMessage(data);
|
|
390
|
+
},
|
|
391
|
+
onError: (error) => {
|
|
392
|
+
this.logger.error("iMessage local watcher error", {
|
|
393
|
+
error: String(error)
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
} else {
|
|
398
|
+
remoteGatewaySdk = new AdvancedIMessageKit({
|
|
399
|
+
serverUrl: this.serverUrl,
|
|
400
|
+
apiKey: this.apiKey
|
|
401
|
+
});
|
|
402
|
+
await remoteGatewaySdk.connect();
|
|
403
|
+
remoteGatewaySdk.on(
|
|
404
|
+
"new-message",
|
|
405
|
+
async (messageResponse) => {
|
|
406
|
+
if (isShuttingDown) return;
|
|
407
|
+
if (messageResponse.isFromMe) return;
|
|
408
|
+
if (isPollVote(messageResponse)) {
|
|
409
|
+
this.handlePollVoteAsModalSubmit(messageResponse, options);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const data = this.normalizeRemoteMessage(messageResponse);
|
|
413
|
+
this.handleGatewayMessage(data);
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
await new Promise((resolve) => {
|
|
418
|
+
const timeout = setTimeout(resolve, durationMs);
|
|
419
|
+
if (abortSignal) {
|
|
420
|
+
if (abortSignal.aborted) {
|
|
421
|
+
clearTimeout(timeout);
|
|
422
|
+
resolve();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
abortSignal.addEventListener(
|
|
426
|
+
"abort",
|
|
427
|
+
() => {
|
|
428
|
+
this.logger.info(
|
|
429
|
+
"iMessage Gateway listener received abort signal"
|
|
430
|
+
);
|
|
431
|
+
clearTimeout(timeout);
|
|
432
|
+
resolve();
|
|
433
|
+
},
|
|
434
|
+
{ once: true }
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
this.logger.info(
|
|
439
|
+
"iMessage Gateway listener duration elapsed, disconnecting"
|
|
440
|
+
);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
this.logger.error("iMessage Gateway listener error", {
|
|
443
|
+
error: String(error)
|
|
444
|
+
});
|
|
445
|
+
} finally {
|
|
446
|
+
isShuttingDown = true;
|
|
447
|
+
if (this.local) {
|
|
448
|
+
this.sdk.stopWatching();
|
|
449
|
+
} else if (remoteGatewaySdk) {
|
|
450
|
+
await remoteGatewaySdk.close();
|
|
451
|
+
}
|
|
452
|
+
this.logger.info("iMessage Gateway listener stopped");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
handlePollVoteAsModalSubmit(messageResponse, options) {
|
|
456
|
+
if (!this.chat) return;
|
|
457
|
+
const pollGuid = messageResponse.associatedMessageGuid;
|
|
458
|
+
if (!pollGuid) {
|
|
459
|
+
this.logger.debug("Poll vote missing associatedMessageGuid, skipping");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const meta = this.modalPollMap.get(pollGuid);
|
|
463
|
+
if (!meta) {
|
|
464
|
+
this.logger.debug("Poll vote for unknown poll, skipping", { pollGuid });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const parsed = parsePollVotes(messageResponse);
|
|
468
|
+
if (!parsed) {
|
|
469
|
+
this.logger.debug("Failed to parse poll votes", {
|
|
470
|
+
guid: messageResponse.guid
|
|
471
|
+
});
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
for (const vote of parsed.votes) {
|
|
475
|
+
const optionIndex = Number.parseInt(vote.voteOptionIdentifier, 10);
|
|
476
|
+
const option = Number.isNaN(optionIndex) ? void 0 : meta.options[optionIndex];
|
|
477
|
+
const value = option?.value ?? vote.voteOptionIdentifier;
|
|
478
|
+
this.chat.processModalSubmit(
|
|
479
|
+
{
|
|
480
|
+
adapter: this,
|
|
481
|
+
callbackId: meta.callbackId,
|
|
482
|
+
privateMetadata: meta.privateMetadata,
|
|
483
|
+
viewId: pollGuid,
|
|
484
|
+
user: {
|
|
485
|
+
userId: vote.participantHandle,
|
|
486
|
+
userName: vote.participantHandle,
|
|
487
|
+
fullName: vote.participantHandle,
|
|
488
|
+
isBot: false,
|
|
489
|
+
isMe: false
|
|
490
|
+
},
|
|
491
|
+
values: { [meta.selectId]: value },
|
|
492
|
+
raw: messageResponse
|
|
493
|
+
},
|
|
494
|
+
meta.contextId,
|
|
495
|
+
options
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async writeTempFiles(files) {
|
|
500
|
+
const dir = await mkdtemp(join(tmpdir(), "imessage-"));
|
|
501
|
+
const paths = [];
|
|
502
|
+
for (const file of files) {
|
|
503
|
+
let buffer;
|
|
504
|
+
if (Buffer.isBuffer(file.data)) {
|
|
505
|
+
buffer = file.data;
|
|
506
|
+
} else if (file.data instanceof Blob) {
|
|
507
|
+
buffer = Buffer.from(await file.data.arrayBuffer());
|
|
508
|
+
} else {
|
|
509
|
+
buffer = Buffer.from(file.data);
|
|
510
|
+
}
|
|
511
|
+
const filePath = join(dir, file.filename);
|
|
512
|
+
await writeFile(filePath, buffer);
|
|
513
|
+
paths.push(filePath);
|
|
514
|
+
}
|
|
515
|
+
return { dir, paths };
|
|
516
|
+
}
|
|
517
|
+
async fetchMessagesLocal(chatGuid, direction, limit, cursor) {
|
|
518
|
+
const sdk = this.sdk;
|
|
519
|
+
const since = direction === "forward" && cursor ? new Date(cursor) : void 0;
|
|
520
|
+
const result = await sdk.getMessages({
|
|
521
|
+
chatId: chatGuid,
|
|
522
|
+
limit: 1e3,
|
|
523
|
+
since
|
|
524
|
+
});
|
|
525
|
+
let messages = [...result.messages].sort(
|
|
526
|
+
(a, b) => a.date.getTime() - b.date.getTime()
|
|
527
|
+
);
|
|
528
|
+
if (direction === "backward" && cursor) {
|
|
529
|
+
const cursorTime = new Date(cursor).getTime();
|
|
530
|
+
messages = messages.filter((m) => m.date.getTime() < cursorTime);
|
|
531
|
+
}
|
|
532
|
+
const isBackward = direction === "backward";
|
|
533
|
+
const start = isBackward ? Math.max(0, messages.length - limit) : 0;
|
|
534
|
+
const selected = messages.slice(start, start + limit);
|
|
535
|
+
const hasMore = isBackward ? start > 0 : messages.length > limit;
|
|
536
|
+
const normalized = selected.map(
|
|
537
|
+
(m) => this.buildMessage(this.normalizeLocalMessage(m))
|
|
538
|
+
);
|
|
539
|
+
let nextCursor;
|
|
540
|
+
if (hasMore && selected.length > 0) {
|
|
541
|
+
nextCursor = isBackward ? selected[0].date.toISOString() : selected.at(-1)?.date.toISOString();
|
|
542
|
+
}
|
|
543
|
+
return { messages: normalized, nextCursor };
|
|
544
|
+
}
|
|
545
|
+
async fetchMessagesRemote(chatGuid, direction, limit, cursor) {
|
|
546
|
+
const sdk = this.sdk;
|
|
547
|
+
const isBackward = direction === "backward";
|
|
548
|
+
const queryOptions = {
|
|
549
|
+
chatGuid,
|
|
550
|
+
limit: limit + 1,
|
|
551
|
+
sort: isBackward ? "DESC" : "ASC",
|
|
552
|
+
with: ["chat", "handle", "attachment"]
|
|
553
|
+
};
|
|
554
|
+
if (cursor) {
|
|
555
|
+
const timestamp = Number(cursor);
|
|
556
|
+
if (isBackward) {
|
|
557
|
+
queryOptions.before = timestamp;
|
|
558
|
+
} else {
|
|
559
|
+
queryOptions.after = timestamp;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const results = await sdk.messages.getMessages(queryOptions);
|
|
563
|
+
const hasMore = results.length > limit;
|
|
564
|
+
const sliced = hasMore ? results.slice(0, limit) : results;
|
|
565
|
+
if (isBackward) {
|
|
566
|
+
sliced.reverse();
|
|
567
|
+
}
|
|
568
|
+
const normalized = sliced.map(
|
|
569
|
+
(m) => this.buildMessage(this.normalizeRemoteMessage(m))
|
|
570
|
+
);
|
|
571
|
+
let nextCursor;
|
|
572
|
+
if (hasMore && sliced.length > 0) {
|
|
573
|
+
nextCursor = isBackward ? String(sliced[0].dateCreated) : String(sliced.at(-1)?.dateCreated);
|
|
574
|
+
}
|
|
575
|
+
return { messages: normalized, nextCursor };
|
|
576
|
+
}
|
|
577
|
+
normalizeLocalMessage(message) {
|
|
578
|
+
return {
|
|
579
|
+
guid: message.guid,
|
|
580
|
+
text: message.text,
|
|
581
|
+
sender: message.sender,
|
|
582
|
+
senderName: message.senderName,
|
|
583
|
+
chatId: message.chatId,
|
|
584
|
+
isGroupChat: message.isGroupChat,
|
|
585
|
+
isFromMe: message.isFromMe,
|
|
586
|
+
date: message.date.toISOString(),
|
|
587
|
+
attachments: message.attachments.map((a) => ({
|
|
588
|
+
id: a.id,
|
|
589
|
+
filename: a.filename,
|
|
590
|
+
mimeType: a.mimeType,
|
|
591
|
+
size: a.size
|
|
592
|
+
})),
|
|
593
|
+
source: "local",
|
|
594
|
+
raw: message
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
normalizeRemoteMessage(messageResponse) {
|
|
598
|
+
const chatGuid = messageResponse.chats?.[0]?.guid ?? "";
|
|
599
|
+
const isGroupChat = !chatGuid.includes(";-;");
|
|
600
|
+
return {
|
|
601
|
+
guid: messageResponse.guid,
|
|
602
|
+
text: messageResponse.text,
|
|
603
|
+
sender: messageResponse.handle?.address ?? "",
|
|
604
|
+
senderName: null,
|
|
605
|
+
chatId: chatGuid,
|
|
606
|
+
isGroupChat,
|
|
607
|
+
isFromMe: messageResponse.isFromMe,
|
|
608
|
+
date: new Date(messageResponse.dateCreated).toISOString(),
|
|
609
|
+
attachments: (messageResponse.attachments ?? []).map((a) => ({
|
|
610
|
+
id: a.guid,
|
|
611
|
+
filename: a.transferName,
|
|
612
|
+
mimeType: a.mimeType ?? "application/octet-stream",
|
|
613
|
+
size: a.totalBytes
|
|
614
|
+
})),
|
|
615
|
+
source: "remote",
|
|
616
|
+
raw: messageResponse
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
buildMessage(data) {
|
|
620
|
+
const threadId = this.encodeThreadId({ chatGuid: data.chatId });
|
|
621
|
+
return new Message({
|
|
622
|
+
id: data.guid,
|
|
623
|
+
threadId,
|
|
624
|
+
text: data.text ?? "",
|
|
625
|
+
formatted: parseMarkdown2(data.text ?? ""),
|
|
626
|
+
author: {
|
|
627
|
+
userId: data.sender,
|
|
628
|
+
userName: data.senderName ?? data.sender,
|
|
629
|
+
fullName: data.senderName ?? data.sender,
|
|
630
|
+
isBot: false,
|
|
631
|
+
isMe: data.isFromMe
|
|
632
|
+
},
|
|
633
|
+
metadata: {
|
|
634
|
+
dateSent: new Date(data.date),
|
|
635
|
+
edited: false
|
|
636
|
+
},
|
|
637
|
+
attachments: data.attachments.map((a) => ({
|
|
638
|
+
type: this.getAttachmentType(a.mimeType),
|
|
639
|
+
name: a.filename,
|
|
640
|
+
mimeType: a.mimeType,
|
|
641
|
+
size: a.size
|
|
642
|
+
})),
|
|
643
|
+
raw: data.raw ?? data,
|
|
644
|
+
isMention: !data.isGroupChat
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
handleGatewayMessage(data, options) {
|
|
648
|
+
if (!this.chat) return;
|
|
649
|
+
const chatMessage = this.buildMessage(data);
|
|
650
|
+
this.chat.processMessage(this, chatMessage.threadId, chatMessage, options);
|
|
651
|
+
}
|
|
652
|
+
emojiToTapback(emoji) {
|
|
653
|
+
const name = typeof emoji === "string" ? emoji : emoji.name;
|
|
654
|
+
const tapbackMap = {
|
|
655
|
+
heart: "love",
|
|
656
|
+
love: "love",
|
|
657
|
+
thumbs_up: "like",
|
|
658
|
+
like: "like",
|
|
659
|
+
thumbs_down: "dislike",
|
|
660
|
+
dislike: "dislike",
|
|
661
|
+
laugh: "laugh",
|
|
662
|
+
emphasize: "emphasize",
|
|
663
|
+
exclamation: "emphasize",
|
|
664
|
+
question: "question"
|
|
665
|
+
};
|
|
666
|
+
const tapback = tapbackMap[name];
|
|
667
|
+
if (!tapback) {
|
|
668
|
+
throw new ValidationError(
|
|
669
|
+
"imessage",
|
|
670
|
+
`Unsupported iMessage tapback: "${name}". Supported: heart, thumbs_up, thumbs_down, laugh, emphasize, question`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return tapback;
|
|
674
|
+
}
|
|
675
|
+
getAttachmentType(mimeType) {
|
|
676
|
+
if (!mimeType) return "file";
|
|
677
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
678
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
679
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
680
|
+
return "file";
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
function createiMessageAdapter(config) {
|
|
684
|
+
const local = config?.local ?? process.env.IMESSAGE_LOCAL !== "false";
|
|
685
|
+
const logger = config?.logger ?? new ConsoleLogger("info").child("imessage");
|
|
686
|
+
if (local) {
|
|
687
|
+
return new iMessageAdapter({
|
|
688
|
+
local: true,
|
|
689
|
+
logger,
|
|
690
|
+
serverUrl: config?.serverUrl ?? process.env.IMESSAGE_SERVER_URL,
|
|
691
|
+
apiKey: config?.apiKey ?? process.env.IMESSAGE_API_KEY
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const serverUrl = config?.serverUrl ?? process.env.IMESSAGE_SERVER_URL;
|
|
695
|
+
if (!serverUrl) {
|
|
696
|
+
throw new ValidationError(
|
|
697
|
+
"imessage",
|
|
698
|
+
"serverUrl is required when local is false. Set IMESSAGE_SERVER_URL or provide it in config."
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const apiKey = config?.apiKey ?? process.env.IMESSAGE_API_KEY;
|
|
702
|
+
if (!apiKey) {
|
|
703
|
+
throw new ValidationError(
|
|
704
|
+
"imessage",
|
|
705
|
+
"apiKey is required when local is false. Set IMESSAGE_API_KEY or provide it in config."
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return new iMessageAdapter({
|
|
709
|
+
local: false,
|
|
710
|
+
logger,
|
|
711
|
+
serverUrl,
|
|
712
|
+
apiKey
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
export {
|
|
716
|
+
createiMessageAdapter,
|
|
717
|
+
iMessageAdapter,
|
|
718
|
+
iMessageFormatConverter
|
|
719
|
+
};
|
|
720
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/markdown.ts"],"sourcesContent":["import { mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { extractFiles, ValidationError } from \"@chat-adapter/shared\";\nimport type { MessageResponse } from \"@photon-ai/advanced-imessage-kit\";\nimport {\n AdvancedIMessageKit,\n isPollVote,\n parsePollVotes,\n} from \"@photon-ai/advanced-imessage-kit\";\nimport type { Message as IMessageLocalMessage } from \"@photon-ai/imessage-kit\";\nimport { IMessageSDK } from \"@photon-ai/imessage-kit\";\nimport type {\n Adapter,\n AdapterPostableMessage,\n ChatInstance,\n EmojiValue,\n FetchOptions,\n FetchResult,\n FileUpload,\n FormattedContent,\n Logger,\n ModalElement,\n RawMessage,\n SelectElement,\n SelectOptionElement,\n ThreadInfo,\n WebhookOptions,\n} from \"chat\";\nimport { ConsoleLogger, Message, NotImplementedError, parseMarkdown } from \"chat\";\nimport { iMessageFormatConverter } from \"./markdown\";\nimport type { iMessageGatewayMessageData, iMessageThreadId } from \"./types\";\n\nexport { iMessageFormatConverter } from \"./markdown\";\nexport type {\n iMessageGatewayMessageData,\n iMessageThreadId,\n NativeWebhookPayload,\n} from \"./types\";\n\nexport interface iMessageAdapterLocalConfig {\n apiKey?: string;\n local: true;\n logger: Logger;\n serverUrl?: string;\n}\n\nexport interface iMessageAdapterRemoteConfig {\n apiKey: string;\n local: false;\n logger: Logger;\n serverUrl: string;\n}\n\nexport type iMessageAdapterConfig =\n | iMessageAdapterLocalConfig\n | iMessageAdapterRemoteConfig;\n\nexport class iMessageAdapter implements Adapter {\n readonly name = \"imessage\";\n readonly userName: string = \"\";\n readonly local: boolean;\n readonly serverUrl?: string;\n readonly apiKey?: string;\n readonly sdk: IMessageSDK | AdvancedIMessageKit;\n\n private chat: ChatInstance | null = null;\n private readonly logger: Logger;\n private readonly formatConverter = new iMessageFormatConverter();\n private readonly modalPollMap = new Map<\n string,\n {\n callbackId: string;\n selectId: string;\n options: SelectOptionElement[];\n contextId?: string;\n privateMetadata?: string;\n }\n >();\n\n constructor(config: iMessageAdapterConfig) {\n if (config.local && process.platform !== \"darwin\") {\n throw new ValidationError(\n \"imessage\",\n \"iMessage adapter local mode requires macOS. Current platform: \" +\n process.platform\n );\n }\n\n this.local = config.local;\n this.serverUrl = config.serverUrl;\n this.apiKey = config.apiKey;\n this.logger = config.logger;\n\n if (config.local) {\n this.sdk = new IMessageSDK();\n } else {\n this.sdk = AdvancedIMessageKit.getInstance({\n serverUrl: config.serverUrl,\n apiKey: config.apiKey,\n });\n }\n }\n\n async initialize(chat: ChatInstance): Promise<void> {\n this.chat = chat;\n this.logger.info(\"iMessage adapter initialized\", {\n local: this.local,\n serverUrl: this.serverUrl ? \"configured\" : \"not configured\",\n });\n\n if (!this.local) {\n const sdk = this.sdk as AdvancedIMessageKit;\n await sdk.connect();\n await new Promise<void>((resolve) => sdk.once(\"ready\", resolve));\n }\n }\n\n async handleWebhook(\n _request: Request,\n _options?: WebhookOptions\n ): Promise<Response> {\n return new Response(\"Webhook not supported -- use startGatewayListener()\", {\n status: 501,\n });\n }\n\n async postMessage(\n threadId: string,\n message: AdapterPostableMessage\n ): Promise<RawMessage> {\n const { chatGuid } = this.decodeThreadId(threadId);\n\n const text = this.formatConverter.renderPostable(message);\n const files = extractFiles(message);\n const tempFiles =\n files.length > 0 ? await this.writeTempFiles(files) : null;\n\n try {\n if (this.local) {\n const sdk = this.sdk as IMessageSDK;\n const recipient = chatGuid.split(\";\").pop() ?? chatGuid;\n const content = tempFiles?.paths.length\n ? { text: text || undefined, files: tempFiles.paths }\n : text;\n const result = await sdk.send(recipient, content);\n return {\n id: result.message?.guid ?? `local-${Date.now()}`,\n threadId,\n raw: result,\n };\n }\n\n const sdk = this.sdk as AdvancedIMessageKit;\n let result: MessageResponse | undefined;\n\n if (text || !tempFiles) {\n result = await sdk.messages.sendMessage({\n chatGuid,\n message: text,\n });\n }\n\n if (tempFiles) {\n for (const filePath of tempFiles.paths) {\n const attachmentResult = await sdk.attachments.sendAttachment({\n chatGuid,\n filePath,\n });\n result ??= attachmentResult;\n }\n }\n\n return {\n id: result?.guid ?? `msg-${Date.now()}`,\n threadId,\n raw: result,\n };\n } finally {\n if (tempFiles) {\n await rm(tempFiles.dir, { recursive: true }).catch(() => {});\n }\n }\n }\n\n async editMessage(\n threadId: string,\n messageId: string,\n message: AdapterPostableMessage\n ): Promise<RawMessage> {\n if (this.local) {\n throw new NotImplementedError(\n \"editMessage is not supported in local mode\",\n \"editMessage\"\n );\n }\n\n const text = this.formatConverter.renderPostable(message);\n const sdk = this.sdk as AdvancedIMessageKit;\n const result = await sdk.messages.editMessage({\n messageGuid: messageId,\n editedMessage: text,\n backwardsCompatibilityMessage: text,\n });\n return {\n id: result.guid,\n threadId,\n raw: result,\n };\n }\n\n async deleteMessage(_threadId: string, _messageId: string): Promise<void> {\n throw new NotImplementedError(\n \"deleteMessage is not implemented\",\n \"deleteMessage\"\n );\n }\n\n parseMessage(raw: unknown): Message {\n const data = this.local\n ? this.normalizeLocalMessage(raw as IMessageLocalMessage)\n : this.normalizeRemoteMessage(raw as MessageResponse);\n return this.buildMessage(data);\n }\n\n async fetchMessages(\n threadId: string,\n options?: FetchOptions\n ): Promise<FetchResult> {\n const { chatGuid } = this.decodeThreadId(threadId);\n const direction = options?.direction ?? \"backward\";\n const limit = options?.limit ?? 50;\n const cursor = options?.cursor;\n\n if (this.local) {\n return this.fetchMessagesLocal(chatGuid, direction, limit, cursor);\n }\n\n return this.fetchMessagesRemote(chatGuid, direction, limit, cursor);\n }\n\n async fetchThread(threadId: string): Promise<ThreadInfo> {\n if (this.local) {\n throw new NotImplementedError(\n \"fetchThread is not supported in local mode\",\n \"fetchThread\"\n );\n }\n\n const { chatGuid } = this.decodeThreadId(threadId);\n const sdk = this.sdk as AdvancedIMessageKit;\n const chat = await sdk.chats.getChat(chatGuid);\n const isDM = chatGuid.includes(\";-;\");\n\n return {\n id: threadId,\n channelId: chatGuid,\n channelName: chat.displayName || undefined,\n isDM,\n metadata: {\n chatIdentifier: chat.chatIdentifier,\n style: chat.style,\n participants: chat.participants,\n isArchived: chat.isArchived,\n raw: chat,\n },\n };\n }\n\n async addReaction(\n threadId: string,\n messageId: string,\n emoji: EmojiValue | string\n ): Promise<void> {\n if (this.local) {\n throw new NotImplementedError(\n \"addReaction is not supported in local mode\",\n \"addReaction\"\n );\n }\n\n const tapback = this.emojiToTapback(emoji);\n const { chatGuid } = this.decodeThreadId(threadId);\n const sdk = this.sdk as AdvancedIMessageKit;\n await sdk.messages.sendReaction({\n chatGuid,\n messageGuid: messageId,\n reaction: tapback,\n });\n }\n\n async removeReaction(\n threadId: string,\n messageId: string,\n emoji: EmojiValue | string\n ): Promise<void> {\n if (this.local) {\n throw new NotImplementedError(\n \"removeReaction is not supported in local mode\",\n \"removeReaction\"\n );\n }\n\n const tapback = this.emojiToTapback(emoji);\n const { chatGuid } = this.decodeThreadId(threadId);\n const sdk = this.sdk as AdvancedIMessageKit;\n await sdk.messages.sendReaction({\n chatGuid,\n messageGuid: messageId,\n reaction: `-${tapback}`,\n });\n }\n\n async startTyping(threadId: string, _status?: string): Promise<void> {\n if (this.local) {\n throw new NotImplementedError(\n \"startTyping is not supported in local mode\",\n \"startTyping\"\n );\n }\n\n const { chatGuid } = this.decodeThreadId(threadId);\n const sdk = this.sdk as AdvancedIMessageKit;\n await sdk.chats.startTyping(chatGuid);\n setTimeout(() => sdk.chats.stopTyping(chatGuid), 3000);\n }\n\n async openModal(\n triggerId: string,\n modal: ModalElement,\n contextId?: string\n ): Promise<{ viewId: string }> {\n if (this.local) {\n throw new NotImplementedError(\n \"openModal is not supported in local mode\",\n \"openModal\"\n );\n }\n\n const select = modal.children.find(\n (c): c is SelectElement => c.type === \"select\"\n );\n if (!select) {\n throw new ValidationError(\n \"imessage\",\n \"openModal requires at least one Select child — iMessage modals map to native polls\"\n );\n }\n\n const { chatGuid } = this.decodeThreadId(triggerId);\n const sdk = this.sdk as AdvancedIMessageKit;\n\n const result = await sdk.polls.create({\n chatGuid,\n title: modal.title,\n options: select.options.map((o) => o.label),\n });\n\n this.modalPollMap.set(result.guid, {\n callbackId: modal.callbackId,\n selectId: select.id,\n options: select.options,\n contextId,\n privateMetadata: modal.privateMetadata,\n });\n\n return { viewId: result.guid };\n }\n\n renderFormatted(content: FormattedContent): string {\n return this.formatConverter.fromAst(content);\n }\n\n encodeThreadId(platformData: iMessageThreadId): string {\n return `imessage:${platformData.chatGuid}`;\n }\n\n decodeThreadId(threadId: string): iMessageThreadId {\n if (!threadId.startsWith(\"imessage:\")) {\n throw new ValidationError(\n \"imessage\",\n `Invalid iMessage thread ID: ${threadId}`\n );\n }\n return { chatGuid: threadId.slice(\"imessage:\".length) };\n }\n\n isDM(threadId: string): boolean {\n const { chatGuid } = this.decodeThreadId(threadId);\n return chatGuid.includes(\";-;\");\n }\n\n async startGatewayListener(\n options: WebhookOptions,\n durationMs = 180000,\n abortSignal?: AbortSignal\n ): Promise<Response> {\n if (!this.chat) {\n return new Response(\"Chat instance not initialized\", { status: 500 });\n }\n\n if (!options.waitUntil) {\n return new Response(\"waitUntil not provided\", { status: 500 });\n }\n\n this.logger.info(\"Starting iMessage Gateway listener\", {\n durationMs,\n mode: this.local ? \"local\" : \"remote\",\n });\n\n const listenerPromise = this.runGatewayListener(durationMs, abortSignal, options);\n options.waitUntil(listenerPromise);\n\n return new Response(\n JSON.stringify({\n status: \"listening\",\n durationMs,\n mode: this.local ? \"local\" : \"remote\",\n message: `Gateway listener started, will run for ${durationMs / 1000} seconds`,\n }),\n {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n }\n );\n }\n\n private async runGatewayListener(\n durationMs: number,\n abortSignal?: AbortSignal,\n options?: WebhookOptions\n ): Promise<void> {\n let isShuttingDown = false;\n let remoteGatewaySdk: AdvancedIMessageKit | null = null;\n\n try {\n if (this.local) {\n const sdk = this.sdk as IMessageSDK;\n await sdk.startWatching({\n onMessage: async (message: IMessageLocalMessage) => {\n if (isShuttingDown) return;\n if (message.isFromMe) return;\n const data = this.normalizeLocalMessage(message);\n this.handleGatewayMessage(data);\n },\n onError: (error: Error) => {\n this.logger.error(\"iMessage local watcher error\", {\n error: String(error),\n });\n },\n });\n } else {\n remoteGatewaySdk = new AdvancedIMessageKit({\n serverUrl: this.serverUrl,\n apiKey: this.apiKey,\n });\n await remoteGatewaySdk.connect();\n\n remoteGatewaySdk.on(\n \"new-message\",\n async (messageResponse: MessageResponse) => {\n if (isShuttingDown) return;\n if (messageResponse.isFromMe) return;\n\n if (isPollVote(messageResponse)) {\n this.handlePollVoteAsModalSubmit(messageResponse, options);\n return;\n }\n\n const data = this.normalizeRemoteMessage(messageResponse);\n this.handleGatewayMessage(data);\n }\n );\n }\n\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(resolve, durationMs);\n if (abortSignal) {\n if (abortSignal.aborted) {\n clearTimeout(timeout);\n resolve();\n return;\n }\n abortSignal.addEventListener(\n \"abort\",\n () => {\n this.logger.info(\n \"iMessage Gateway listener received abort signal\"\n );\n clearTimeout(timeout);\n resolve();\n },\n { once: true }\n );\n }\n });\n\n this.logger.info(\n \"iMessage Gateway listener duration elapsed, disconnecting\"\n );\n } catch (error) {\n this.logger.error(\"iMessage Gateway listener error\", {\n error: String(error),\n });\n } finally {\n isShuttingDown = true;\n if (this.local) {\n (this.sdk as IMessageSDK).stopWatching();\n } else if (remoteGatewaySdk) {\n await remoteGatewaySdk.close();\n }\n this.logger.info(\"iMessage Gateway listener stopped\");\n }\n }\n\n private handlePollVoteAsModalSubmit(\n messageResponse: MessageResponse,\n options?: WebhookOptions\n ): void {\n if (!this.chat) return;\n\n const pollGuid = messageResponse.associatedMessageGuid;\n if (!pollGuid) {\n this.logger.debug(\"Poll vote missing associatedMessageGuid, skipping\");\n return;\n }\n\n const meta = this.modalPollMap.get(pollGuid);\n if (!meta) {\n this.logger.debug(\"Poll vote for unknown poll, skipping\", { pollGuid });\n return;\n }\n\n const parsed = parsePollVotes(messageResponse);\n if (!parsed) {\n this.logger.debug(\"Failed to parse poll votes\", {\n guid: messageResponse.guid,\n });\n return;\n }\n\n for (const vote of parsed.votes) {\n const optionIndex = Number.parseInt(vote.voteOptionIdentifier, 10);\n const option = Number.isNaN(optionIndex)\n ? undefined\n : meta.options[optionIndex];\n const value = option?.value ?? vote.voteOptionIdentifier;\n\n this.chat.processModalSubmit(\n {\n adapter: this,\n callbackId: meta.callbackId,\n privateMetadata: meta.privateMetadata,\n viewId: pollGuid,\n user: {\n userId: vote.participantHandle,\n userName: vote.participantHandle,\n fullName: vote.participantHandle,\n isBot: false,\n isMe: false,\n },\n values: { [meta.selectId]: value },\n raw: messageResponse,\n },\n meta.contextId,\n options\n );\n }\n }\n\n private async writeTempFiles(\n files: FileUpload[]\n ): Promise<{ dir: string; paths: string[] }> {\n const dir = await mkdtemp(join(tmpdir(), \"imessage-\"));\n const paths: string[] = [];\n for (const file of files) {\n let buffer: Buffer;\n if (Buffer.isBuffer(file.data)) {\n buffer = file.data;\n } else if (file.data instanceof Blob) {\n buffer = Buffer.from(await file.data.arrayBuffer());\n } else {\n buffer = Buffer.from(file.data as ArrayBuffer);\n }\n const filePath = join(dir, file.filename);\n await writeFile(filePath, buffer);\n paths.push(filePath);\n }\n return { dir, paths };\n }\n\n private async fetchMessagesLocal(\n chatGuid: string,\n direction: \"forward\" | \"backward\",\n limit: number,\n cursor?: string\n ): Promise<FetchResult> {\n const sdk = this.sdk as IMessageSDK;\n const since =\n direction === \"forward\" && cursor ? new Date(cursor) : undefined;\n const result = await sdk.getMessages({\n chatId: chatGuid,\n limit: 1000,\n since,\n });\n\n let messages = [...result.messages].sort(\n (a, b) => a.date.getTime() - b.date.getTime()\n );\n\n if (direction === \"backward\" && cursor) {\n const cursorTime = new Date(cursor).getTime();\n messages = messages.filter((m) => m.date.getTime() < cursorTime);\n }\n\n const isBackward = direction === \"backward\";\n const start = isBackward ? Math.max(0, messages.length - limit) : 0;\n const selected = messages.slice(start, start + limit);\n const hasMore = isBackward ? start > 0 : messages.length > limit;\n\n const normalized = selected.map((m) =>\n this.buildMessage(this.normalizeLocalMessage(m))\n );\n\n let nextCursor: string | undefined;\n if (hasMore && selected.length > 0) {\n nextCursor = isBackward\n ? selected[0].date.toISOString()\n : selected.at(-1)?.date.toISOString();\n }\n\n return { messages: normalized, nextCursor };\n }\n\n private async fetchMessagesRemote(\n chatGuid: string,\n direction: \"forward\" | \"backward\",\n limit: number,\n cursor?: string\n ): Promise<FetchResult> {\n const sdk = this.sdk as AdvancedIMessageKit;\n const isBackward = direction === \"backward\";\n\n const queryOptions: {\n chatGuid: string;\n limit: number;\n sort: \"ASC\" | \"DESC\";\n before?: number;\n after?: number;\n with?: string[];\n } = {\n chatGuid,\n limit: limit + 1,\n sort: isBackward ? \"DESC\" : \"ASC\",\n with: [\"chat\", \"handle\", \"attachment\"],\n };\n\n if (cursor) {\n const timestamp = Number(cursor);\n if (isBackward) {\n queryOptions.before = timestamp;\n } else {\n queryOptions.after = timestamp;\n }\n }\n\n const results = await sdk.messages.getMessages(queryOptions);\n const hasMore = results.length > limit;\n const sliced = hasMore ? results.slice(0, limit) : results;\n\n if (isBackward) {\n sliced.reverse();\n }\n\n const normalized = sliced.map((m) =>\n this.buildMessage(this.normalizeRemoteMessage(m))\n );\n\n let nextCursor: string | undefined;\n if (hasMore && sliced.length > 0) {\n nextCursor = isBackward\n ? String(sliced[0].dateCreated)\n : String(sliced.at(-1)?.dateCreated);\n }\n\n return { messages: normalized, nextCursor };\n }\n\n private normalizeLocalMessage(\n message: IMessageLocalMessage\n ): iMessageGatewayMessageData {\n return {\n guid: message.guid,\n text: message.text,\n sender: message.sender,\n senderName: message.senderName,\n chatId: message.chatId,\n isGroupChat: message.isGroupChat,\n isFromMe: message.isFromMe,\n date: message.date.toISOString(),\n attachments: message.attachments.map((a) => ({\n id: a.id,\n filename: a.filename,\n mimeType: a.mimeType,\n size: a.size,\n })),\n source: \"local\",\n raw: message,\n };\n }\n\n private normalizeRemoteMessage(\n messageResponse: MessageResponse\n ): iMessageGatewayMessageData {\n const chatGuid = messageResponse.chats?.[0]?.guid ?? \"\";\n const isGroupChat = !chatGuid.includes(\";-;\");\n\n return {\n guid: messageResponse.guid,\n text: messageResponse.text,\n sender: messageResponse.handle?.address ?? \"\",\n senderName: null,\n chatId: chatGuid,\n isGroupChat,\n isFromMe: messageResponse.isFromMe,\n date: new Date(messageResponse.dateCreated).toISOString(),\n attachments: (messageResponse.attachments ?? []).map((a) => ({\n id: a.guid,\n filename: a.transferName,\n mimeType: a.mimeType ?? \"application/octet-stream\",\n size: a.totalBytes,\n })),\n source: \"remote\",\n raw: messageResponse,\n };\n }\n\n private buildMessage(data: iMessageGatewayMessageData): Message {\n const threadId = this.encodeThreadId({ chatGuid: data.chatId });\n return new Message({\n id: data.guid,\n threadId,\n text: data.text ?? \"\",\n formatted: parseMarkdown(data.text ?? \"\"),\n author: {\n userId: data.sender,\n userName: data.senderName ?? data.sender,\n fullName: data.senderName ?? data.sender,\n isBot: false,\n isMe: data.isFromMe,\n },\n metadata: {\n dateSent: new Date(data.date),\n edited: false,\n },\n attachments: data.attachments.map((a) => ({\n type: this.getAttachmentType(a.mimeType),\n name: a.filename,\n mimeType: a.mimeType,\n size: a.size,\n })),\n raw: data.raw ?? data,\n isMention: !data.isGroupChat,\n });\n }\n\n private handleGatewayMessage(\n data: iMessageGatewayMessageData,\n options?: WebhookOptions\n ): void {\n if (!this.chat) return;\n const chatMessage = this.buildMessage(data);\n this.chat.processMessage(this, chatMessage.threadId, chatMessage, options);\n }\n\n private emojiToTapback(emoji: EmojiValue | string): string {\n const name = typeof emoji === \"string\" ? emoji : emoji.name;\n const tapbackMap: Record<string, string> = {\n heart: \"love\",\n love: \"love\",\n thumbs_up: \"like\",\n like: \"like\",\n thumbs_down: \"dislike\",\n dislike: \"dislike\",\n laugh: \"laugh\",\n emphasize: \"emphasize\",\n exclamation: \"emphasize\",\n question: \"question\",\n };\n const tapback = tapbackMap[name];\n if (!tapback) {\n throw new ValidationError(\n \"imessage\",\n `Unsupported iMessage tapback: \"${name}\". Supported: heart, thumbs_up, thumbs_down, laugh, emphasize, question`\n );\n }\n return tapback;\n }\n\n private getAttachmentType(\n mimeType?: string\n ): \"image\" | \"video\" | \"audio\" | \"file\" {\n if (!mimeType) return \"file\";\n if (mimeType.startsWith(\"image/\")) return \"image\";\n if (mimeType.startsWith(\"video/\")) return \"video\";\n if (mimeType.startsWith(\"audio/\")) return \"audio\";\n return \"file\";\n }\n}\n\nexport function createiMessageAdapter(\n config?: Partial<iMessageAdapterConfig>\n): iMessageAdapter {\n const local = config?.local ?? process.env.IMESSAGE_LOCAL !== \"false\";\n const logger = config?.logger ?? new ConsoleLogger(\"info\").child(\"imessage\");\n\n if (local) {\n return new iMessageAdapter({\n local: true,\n logger,\n serverUrl: config?.serverUrl ?? process.env.IMESSAGE_SERVER_URL,\n apiKey: config?.apiKey ?? process.env.IMESSAGE_API_KEY,\n });\n }\n\n const serverUrl = config?.serverUrl ?? process.env.IMESSAGE_SERVER_URL;\n if (!serverUrl) {\n throw new ValidationError(\n \"imessage\",\n \"serverUrl is required when local is false. Set IMESSAGE_SERVER_URL or provide it in config.\"\n );\n }\n\n const apiKey = config?.apiKey ?? process.env.IMESSAGE_API_KEY;\n if (!apiKey) {\n throw new ValidationError(\n \"imessage\",\n \"apiKey is required when local is false. Set IMESSAGE_API_KEY or provide it in config.\"\n );\n }\n\n return new iMessageAdapter({\n local: false,\n logger,\n serverUrl,\n apiKey,\n });\n}\n","/**\n * iMessage format conversion using AST-based parsing.\n *\n * iMessage supports plain text only -- no rich formatting syntax.\n * The converter strips formatting markers and outputs clean plain text,\n * preserving structure (lists, blockquotes, code blocks) with whitespace.\n */\n\nimport {\n BaseFormatConverter,\n type Content,\n getNodeChildren,\n getNodeValue,\n isBlockquoteNode,\n isCodeNode,\n isDeleteNode,\n isEmphasisNode,\n isInlineCodeNode,\n isLinkNode,\n isListItemNode,\n isListNode,\n isParagraphNode,\n isStrongNode,\n isTextNode,\n parseMarkdown,\n type Root,\n} from \"chat\";\n\nexport class iMessageFormatConverter extends BaseFormatConverter {\n /**\n * Render an AST to iMessage plain text format.\n * Strips all formatting markers since iMessage doesn't support rich text via API.\n */\n fromAst(ast: Root): string {\n return this.fromAstWithNodeConverter(ast, (node) =>\n this.nodeToPlainText(node)\n );\n }\n\n /**\n * Parse iMessage text into an AST.\n * iMessage sends plain text, so we just parse it as markdown.\n */\n toAst(text: string): Root {\n return parseMarkdown(text);\n }\n\n private nodeToPlainText(node: Content): string {\n if (isParagraphNode(node)) {\n return getNodeChildren(node)\n .map((child) => this.nodeToPlainText(child))\n .join(\"\");\n }\n\n if (isTextNode(node)) {\n return node.value;\n }\n\n if (isStrongNode(node) || isEmphasisNode(node) || isDeleteNode(node)) {\n return getNodeChildren(node)\n .map((child) => this.nodeToPlainText(child))\n .join(\"\");\n }\n\n if (isInlineCodeNode(node)) {\n return node.value;\n }\n\n if (isCodeNode(node)) {\n return node.value;\n }\n\n if (isLinkNode(node)) {\n const linkText = getNodeChildren(node)\n .map((child) => this.nodeToPlainText(child))\n .join(\"\");\n return linkText ? `${linkText} (${node.url})` : node.url;\n }\n\n if (isBlockquoteNode(node)) {\n return getNodeChildren(node)\n .map((child) => `> ${this.nodeToPlainText(child)}`)\n .join(\"\\n\");\n }\n\n if (isListNode(node)) {\n return getNodeChildren(node)\n .map((item, i) => {\n const prefix = node.ordered ? `${i + 1}.` : \"-\";\n const content = getNodeChildren(item)\n .map((child) => this.nodeToPlainText(child))\n .join(\"\");\n return `${prefix} ${content}`;\n })\n .join(\"\\n\");\n }\n\n if (isListItemNode(node)) {\n return getNodeChildren(node)\n .map((child) => this.nodeToPlainText(child))\n .join(\"\");\n }\n\n if (node.type === \"break\") {\n return \"\\n\";\n }\n\n if (node.type === \"thematicBreak\") {\n return \"---\";\n }\n\n const children = getNodeChildren(node);\n if (children.length > 0) {\n return children.map((child) => this.nodeToPlainText(child)).join(\"\");\n }\n return getNodeValue(node);\n }\n}\n"],"mappings":"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","names":["parseMarkdown","sdk","result","parseMarkdown"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chat-adapter-imessage",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "iMessage adapter for Chat SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"test": "vitest run --coverage",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"clean": "rm -rf dist coverage"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"chat": "^4.14.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@chat-adapter/shared": "^4.15.0",
|
|
31
|
+
"@photon-ai/advanced-imessage-kit": "^1.14.3",
|
|
32
|
+
"@photon-ai/imessage-kit": "^2.1.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.10.2",
|
|
36
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
37
|
+
"chat": "^4.15.0",
|
|
38
|
+
"tsup": "^8.3.5",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "^4.0.18"
|
|
41
|
+
},
|
|
42
|
+
"packageManager": "pnpm@10.30.3",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"chat-sdk",
|
|
48
|
+
"chat-adapter",
|
|
49
|
+
"imessage",
|
|
50
|
+
"sms",
|
|
51
|
+
"bot",
|
|
52
|
+
"adapter"
|
|
53
|
+
],
|
|
8
54
|
"license": "MIT"
|
|
9
55
|
}
|
package/index.js
DELETED