@spectrum-ts/imessage-local 10.0.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 +20 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +536 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License Copyright (c) 2025 Photon AI
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted,
|
|
4
|
+
free of charge, to any person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice
|
|
12
|
+
(including the next paragraph) shall be included in all copies or substantial
|
|
13
|
+
portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
18
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# `@spectrum-ts/imessage-local`
|
|
2
|
+
|
|
3
|
+
Local macOS iMessage provider for spectrum-ts, powered by `@photon-ai/imessage-kit`.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bun add spectrum-ts @spectrum-ts/imessage-local
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { imessage } from "@spectrum-ts/imessage-local";
|
|
11
|
+
import { Spectrum } from "spectrum-ts";
|
|
12
|
+
|
|
13
|
+
const spectrum = Spectrum({
|
|
14
|
+
platforms: [imessage.config()],
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This package is intentionally not included in the batteries-included
|
|
19
|
+
`spectrum-ts` package. Install it only on the macOS host that will access the
|
|
20
|
+
local Messages database.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
2
|
+
import { Attachment, ContentBuilder, ContentInput, SchemaMessage, Space, read } from "@spectrum-ts/core";
|
|
3
|
+
import { PhotoInput } from "@spectrum-ts/core/authoring";
|
|
4
|
+
import z from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/types.d.ts
|
|
7
|
+
declare const userSchema: z.ZodObject<{
|
|
8
|
+
address: z.ZodOptional<z.ZodString>;
|
|
9
|
+
country: z.ZodOptional<z.ZodString>;
|
|
10
|
+
service: z.ZodOptional<z.ZodEnum<{
|
|
11
|
+
iMessage: "iMessage";
|
|
12
|
+
SMS: "SMS";
|
|
13
|
+
RCS: "RCS";
|
|
14
|
+
unknown: "unknown";
|
|
15
|
+
}>>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
declare const spaceSchema: z.ZodObject<{
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
type: z.ZodEnum<{
|
|
20
|
+
dm: "dm";
|
|
21
|
+
group: "group";
|
|
22
|
+
}>;
|
|
23
|
+
phone: z.ZodString;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
type IMessageMessage = SchemaMessage<typeof userSchema, typeof spaceSchema> & {
|
|
26
|
+
direction?: "inbound" | "outbound";
|
|
27
|
+
partIndex?: number;
|
|
28
|
+
parentId?: string;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region ../imessage/src/content/background.d.ts
|
|
32
|
+
type BackgroundInput = PhotoInput;
|
|
33
|
+
/**
|
|
34
|
+
* Set or clear the chat background. iMessage-only, remote-only.
|
|
35
|
+
*
|
|
36
|
+
* - `background("clear")` — remove the current chat background.
|
|
37
|
+
* - `background("./photo.jpg")` — set background from a filesystem path.
|
|
38
|
+
* MIME type is inferred from the extension; override with `options.mimeType`.
|
|
39
|
+
* - `background(new URL("https://…/photo.jpg"))` — fetch the background
|
|
40
|
+
* lazily over the network. Bytes stay in memory (safe in read-only
|
|
41
|
+
* environments). MIME type is inferred from the URL pathname extension;
|
|
42
|
+
* override with `options.mimeType` when the URL has no usable extension.
|
|
43
|
+
* - `background(buffer, { mimeType })` — set background from in-memory bytes.
|
|
44
|
+
* `options.mimeType` is required.
|
|
45
|
+
*
|
|
46
|
+
* `"clear"` is a reserved string-literal sentinel. If you have a file literally
|
|
47
|
+
* named `clear` with no extension, pass `"./clear"` or load it as a Buffer.
|
|
48
|
+
*
|
|
49
|
+
* `space.send(background(...))` is the canonical form; `space.background(...)`
|
|
50
|
+
* is sugar attached via `PlatformDef.space.actions` (only typed on
|
|
51
|
+
* `PlatformSpace<IMessageDef>`).
|
|
52
|
+
*
|
|
53
|
+
* `Background` is intentionally not a member of the universal `Content`
|
|
54
|
+
* union — the `as unknown as Content` cast keeps the builder shape compatible
|
|
55
|
+
* with the framework's `ContentBuilder.build(): Promise<Content>` signature.
|
|
56
|
+
* The framework treats it as a fire-and-forget control signal at runtime.
|
|
57
|
+
*/
|
|
58
|
+
declare function background(input: "clear"): ContentBuilder;
|
|
59
|
+
declare function background(input: string | Buffer | URL, options?: {
|
|
60
|
+
mimeType?: string;
|
|
61
|
+
}): ContentBuilder;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region ../imessage/src/content/contact-card.d.ts
|
|
64
|
+
/**
|
|
65
|
+
* iMessage-only "share contact card" control signal. Pushes the *local
|
|
66
|
+
* account's native contact card* (the name + photo a recipient sees in their
|
|
67
|
+
* Messages app) to a chat via the SDK's `chats.shareContactInfo`.
|
|
68
|
+
*
|
|
69
|
+
* This is Apple's "Share Name and Photo" mechanism — distinct from the
|
|
70
|
+
* universal `contact(...)` content, which uploads an arbitrary person's vCard
|
|
71
|
+
* as a *file* attachment. There is no payload: the card shared is always the
|
|
72
|
+
* bot account's own.
|
|
73
|
+
*
|
|
74
|
+
* Like `background`, it lives entirely under the iMessage provider and never
|
|
75
|
+
* enters the universal `Content` discriminated union. The framework recognizes
|
|
76
|
+
* it via two generic content-level contracts:
|
|
77
|
+
*
|
|
78
|
+
* 1. `__platform: "iMessage"` — `findUnsupportedPlatformContent` in
|
|
79
|
+
* `platform/build.ts` reads this tag and warns-and-skips when a different
|
|
80
|
+
* platform receives it.
|
|
81
|
+
* 2. `__fireAndForget: true` — `dispatchSend`'s fire-and-forget check treats
|
|
82
|
+
* this as a side-effecting send that returns no message id, the same way it
|
|
83
|
+
* treats `read` / `typing`.
|
|
84
|
+
*
|
|
85
|
+
* iMessage's `send` handler narrows back via the `isContactCard` type guard
|
|
86
|
+
* before dispatching to `chats.shareContactInfo`.
|
|
87
|
+
*/
|
|
88
|
+
declare const contactCardSchema: z.ZodObject<{
|
|
89
|
+
type: z.ZodLiteral<"contactCard">;
|
|
90
|
+
__platform: z.ZodLiteral<"iMessage">;
|
|
91
|
+
__fireAndForget: z.ZodLiteral<true>;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
type ContactCard = z.infer<typeof contactCardSchema>;
|
|
94
|
+
/**
|
|
95
|
+
* Share the bot account's native iMessage contact card (name + photo) with the
|
|
96
|
+
* chat. iMessage-only, remote-only.
|
|
97
|
+
*
|
|
98
|
+
* `space.send(nativeContactCard())` is the canonical form; `space.shareContactCard()`
|
|
99
|
+
* is sugar attached via `PlatformDef.space.actions` (only typed on
|
|
100
|
+
* `PlatformSpace<IMessageDef>`).
|
|
101
|
+
*
|
|
102
|
+
* This is an explicit, on-demand share and always fires — unlike the automatic
|
|
103
|
+
* best-effort share gated behind the `imessageSynced` project profile, which
|
|
104
|
+
* dedupes to once per chat per 24h (see `remote/contact-share.ts`). Works in
|
|
105
|
+
* both DMs and group chats; the recipient chooses whether to accept the card.
|
|
106
|
+
*
|
|
107
|
+
* `ContactCard` is intentionally not a member of the universal `Content`
|
|
108
|
+
* union — the `as unknown as Content` cast keeps the builder shape compatible
|
|
109
|
+
* with the framework's `ContentBuilder.build(): Promise<Content>` signature.
|
|
110
|
+
* The framework treats it as a fire-and-forget control signal at runtime.
|
|
111
|
+
*/
|
|
112
|
+
declare function nativeContactCard(): ContentBuilder;
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region ../imessage/src/content/customized-mini-app.d.ts
|
|
115
|
+
declare const layoutSchema: z.ZodObject<{
|
|
116
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
117
|
+
subcaption: z.ZodOptional<z.ZodString>;
|
|
118
|
+
trailingCaption: z.ZodOptional<z.ZodString>;
|
|
119
|
+
trailingSubcaption: z.ZodOptional<z.ZodString>;
|
|
120
|
+
image: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
121
|
+
imageTitle: z.ZodOptional<z.ZodString>;
|
|
122
|
+
imageSubtitle: z.ZodOptional<z.ZodString>;
|
|
123
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
/**
|
|
126
|
+
* iMessage-only mini-app card content. Lives entirely under the iMessage
|
|
127
|
+
* provider — never enters the universal `Content` discriminated union. The
|
|
128
|
+
* framework recognizes it via the generic content-level platform contract:
|
|
129
|
+
*
|
|
130
|
+
* - `__platform: "iMessage"` — `findUnsupportedPlatformContent` reads this tag
|
|
131
|
+
* and warns-and-skips when a different platform receives it.
|
|
132
|
+
*
|
|
133
|
+
* Unlike `background` / `read`, this content is **not** `__fireAndForget`: it
|
|
134
|
+
* produces a real outbound message, so the iMessage `send` handler narrows
|
|
135
|
+
* back to `CustomizedMiniApp` via the `isCustomizedMiniApp` guard and returns
|
|
136
|
+
* the resulting `ProviderMessageRecord` (rather than `void`).
|
|
137
|
+
*/
|
|
138
|
+
declare const customizedMiniAppSchema: z.ZodObject<{
|
|
139
|
+
type: z.ZodLiteral<"customized-mini-app">;
|
|
140
|
+
__platform: z.ZodLiteral<"iMessage">;
|
|
141
|
+
appName: z.ZodString;
|
|
142
|
+
appStoreId: z.ZodOptional<z.ZodNumber>;
|
|
143
|
+
extensionBundleId: z.ZodString;
|
|
144
|
+
layout: z.ZodObject<{
|
|
145
|
+
caption: z.ZodOptional<z.ZodString>;
|
|
146
|
+
subcaption: z.ZodOptional<z.ZodString>;
|
|
147
|
+
trailingCaption: z.ZodOptional<z.ZodString>;
|
|
148
|
+
trailingSubcaption: z.ZodOptional<z.ZodString>;
|
|
149
|
+
image: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
150
|
+
imageTitle: z.ZodOptional<z.ZodString>;
|
|
151
|
+
imageSubtitle: z.ZodOptional<z.ZodString>;
|
|
152
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
153
|
+
}, z.core.$strip>;
|
|
154
|
+
live: z.ZodOptional<z.ZodBoolean>;
|
|
155
|
+
teamId: z.ZodString;
|
|
156
|
+
url: z.ZodURL;
|
|
157
|
+
}, z.core.$strip>;
|
|
158
|
+
type CustomizedMiniApp = z.infer<typeof customizedMiniAppSchema>;
|
|
159
|
+
type CustomizedMiniAppLayout = z.infer<typeof layoutSchema>;
|
|
160
|
+
type CustomizedMiniAppInput = Omit<CustomizedMiniApp, "type" | "__platform">;
|
|
161
|
+
/**
|
|
162
|
+
* Construct a `customized-mini-app` content value. iMessage-only, remote-only.
|
|
163
|
+
*
|
|
164
|
+
* The layout is what recipients see in the bubble. `teamId` and
|
|
165
|
+
* `extensionBundleId` identify the iMessage extension that receives `url` when
|
|
166
|
+
* the recipient taps the card; the server constructs the matching
|
|
167
|
+
* `MSMessageExtensionBalloonPlugin` plugin id from these values. `appStoreId`
|
|
168
|
+
* is optional and only points recipients without the extension at its App
|
|
169
|
+
* Store entry. `live` is optional; when omitted, the remote server keeps the
|
|
170
|
+
* static layout preview visible.
|
|
171
|
+
*
|
|
172
|
+
* `space.send(customizedMiniApp(...))` is the canonical form.
|
|
173
|
+
*
|
|
174
|
+
* `CustomizedMiniApp` is intentionally not a member of the universal `Content`
|
|
175
|
+
* union — the `as unknown as Content` cast keeps the builder shape compatible
|
|
176
|
+
* with the framework's `ContentBuilder.build(): Promise<Content>` signature.
|
|
177
|
+
*/
|
|
178
|
+
declare function customizedMiniApp(input: CustomizedMiniAppInput): ContentBuilder;
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region ../imessage/src/content/effect.d.ts
|
|
181
|
+
declare const messageEffects: {
|
|
182
|
+
readonly balloons: "com.apple.messages.effect.CKBalloonEffect";
|
|
183
|
+
readonly celebration: "com.apple.messages.effect.CKHappyBirthdayEffect";
|
|
184
|
+
readonly confetti: "com.apple.messages.effect.CKConfettiEffect";
|
|
185
|
+
readonly echo: "com.apple.messages.effect.CKEchoEffect";
|
|
186
|
+
readonly fireworks: "com.apple.messages.effect.CKFireworksEffect";
|
|
187
|
+
readonly gentle: "com.apple.MobileSMS.expressivesend.gentle";
|
|
188
|
+
readonly heart: "com.apple.messages.effect.CKHeartEffect";
|
|
189
|
+
readonly invisible: "com.apple.MobileSMS.expressivesend.invisibleink";
|
|
190
|
+
readonly lasers: "com.apple.messages.effect.CKLasersEffect";
|
|
191
|
+
readonly loud: "com.apple.MobileSMS.expressivesend.loud";
|
|
192
|
+
readonly slam: "com.apple.MobileSMS.expressivesend.impact";
|
|
193
|
+
readonly sparkles: "com.apple.messages.effect.CKSparklesEffect";
|
|
194
|
+
readonly spotlight: "com.apple.messages.effect.CKSpotlightEffect";
|
|
195
|
+
};
|
|
196
|
+
type IMessageMessageEffect = (typeof messageEffects)[keyof typeof messageEffects];
|
|
197
|
+
declare function effect(input: ContentInput, messageEffect: IMessageMessageEffect): ContentBuilder;
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/index.d.ts
|
|
200
|
+
declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"iMessage", import("zod").ZodObject<{}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
|
|
201
|
+
address: import("zod").ZodOptional<import("zod").ZodString>;
|
|
202
|
+
country: import("zod").ZodOptional<import("zod").ZodString>;
|
|
203
|
+
service: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
204
|
+
iMessage: "iMessage";
|
|
205
|
+
SMS: "SMS";
|
|
206
|
+
RCS: "RCS";
|
|
207
|
+
unknown: "unknown";
|
|
208
|
+
}>>;
|
|
209
|
+
}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
|
|
210
|
+
id: import("zod").ZodString;
|
|
211
|
+
type: import("zod").ZodEnum<{
|
|
212
|
+
dm: "dm";
|
|
213
|
+
group: "group";
|
|
214
|
+
}>;
|
|
215
|
+
phone: import("zod").ZodString;
|
|
216
|
+
}, import("zod/v4/core").$strip>, import("zod").ZodObject<{}, import("zod/v4/core").$strip>, IMessageSDK, {
|
|
217
|
+
id: string;
|
|
218
|
+
}, {
|
|
219
|
+
id: string;
|
|
220
|
+
type: "dm" | "group";
|
|
221
|
+
phone: string;
|
|
222
|
+
}, import("zod").ZodObject<{
|
|
223
|
+
partIndex: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
224
|
+
parentId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
225
|
+
}, import("zod/v4/core").$strip>, IMessageMessage, undefined, {
|
|
226
|
+
background: (space: Space, input: BackgroundInput, options?: {
|
|
227
|
+
mimeType?: string;
|
|
228
|
+
}) => Promise<void>;
|
|
229
|
+
shareContactCard: (space: Space) => Promise<void>;
|
|
230
|
+
}, Record<never, never>, {
|
|
231
|
+
getMessage: ({
|
|
232
|
+
client
|
|
233
|
+
}: {
|
|
234
|
+
client: IMessageSDK;
|
|
235
|
+
config: Record<string, never>;
|
|
236
|
+
store: import("@spectrum-ts/core").Store;
|
|
237
|
+
}, _space: {
|
|
238
|
+
id: string;
|
|
239
|
+
type: "dm" | "group";
|
|
240
|
+
phone: string;
|
|
241
|
+
} & {
|
|
242
|
+
id: string;
|
|
243
|
+
__platform: string;
|
|
244
|
+
}, messageId: string) => Promise<IMessageMessage | undefined>;
|
|
245
|
+
getMembers: () => Promise<never>;
|
|
246
|
+
getAvatar: () => Promise<never>;
|
|
247
|
+
getDisplayName: () => Promise<never>;
|
|
248
|
+
getAttachment: () => Promise<Attachment | undefined>;
|
|
249
|
+
}>> & Readonly<{
|
|
250
|
+
effect: {
|
|
251
|
+
message: {
|
|
252
|
+
readonly balloons: "com.apple.messages.effect.CKBalloonEffect";
|
|
253
|
+
readonly celebration: "com.apple.messages.effect.CKHappyBirthdayEffect";
|
|
254
|
+
readonly confetti: "com.apple.messages.effect.CKConfettiEffect";
|
|
255
|
+
readonly echo: "com.apple.messages.effect.CKEchoEffect";
|
|
256
|
+
readonly fireworks: "com.apple.messages.effect.CKFireworksEffect";
|
|
257
|
+
readonly gentle: "com.apple.MobileSMS.expressivesend.gentle";
|
|
258
|
+
readonly heart: "com.apple.messages.effect.CKHeartEffect";
|
|
259
|
+
readonly invisible: "com.apple.MobileSMS.expressivesend.invisibleink";
|
|
260
|
+
readonly lasers: "com.apple.messages.effect.CKLasersEffect";
|
|
261
|
+
readonly loud: "com.apple.MobileSMS.expressivesend.loud";
|
|
262
|
+
readonly slam: "com.apple.MobileSMS.expressivesend.impact";
|
|
263
|
+
readonly sparkles: "com.apple.messages.effect.CKSparklesEffect";
|
|
264
|
+
readonly spotlight: "com.apple.messages.effect.CKSpotlightEffect";
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
}>;
|
|
268
|
+
//#endregion
|
|
269
|
+
export { type BackgroundInput, type ContactCard, type CustomizedMiniApp, type CustomizedMiniAppInput, type CustomizedMiniAppLayout, type IMessageMessageEffect, background, customizedMiniApp, effect, imessage, nativeContactCard, read };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
2
|
+
import { UnsupportedError, appLayoutSchema, definePlatform, fromVCard, read, stream, text, toVCard } from "@spectrum-ts/core";
|
|
3
|
+
import { asAttachment, asContact, asCustom, asReply, asText, buildPhotoAction, messageEffectSchema, photoActionSchema } from "@spectrum-ts/core/authoring";
|
|
4
|
+
import z from "zod";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
|
+
import { createReadStream } from "node:fs";
|
|
7
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { basename, join } from "node:path";
|
|
11
|
+
//#region ../imessage/src/content/background.ts
|
|
12
|
+
/**
|
|
13
|
+
* iMessage-only chat background content. Lives entirely under the iMessage
|
|
14
|
+
* provider — never enters the universal `Content` discriminated union. The
|
|
15
|
+
* framework recognizes it via two generic content-level contracts:
|
|
16
|
+
*
|
|
17
|
+
* 1. `__platform: "iMessage"` — `findUnsupportedPlatformContent` in
|
|
18
|
+
* `platform/build.ts` reads this tag and warns-and-skips when a different
|
|
19
|
+
* platform receives it.
|
|
20
|
+
* 2. `__fireAndForget: true` — `dispatchSend`'s fire-and-forget check
|
|
21
|
+
* treats this as a side-effecting send that returns no message id, the
|
|
22
|
+
* same way it treats `reaction` / `typing` / `edit`.
|
|
23
|
+
*
|
|
24
|
+
* iMessage's `send` handler narrows back to `Background` via the `isBackground`
|
|
25
|
+
* type guard before dispatching to `chats.setBackground` / `removeBackground`.
|
|
26
|
+
*/
|
|
27
|
+
const backgroundSchema = z.object({
|
|
28
|
+
type: z.literal("background"),
|
|
29
|
+
__platform: z.literal("iMessage"),
|
|
30
|
+
__fireAndForget: z.literal(true),
|
|
31
|
+
action: photoActionSchema
|
|
32
|
+
});
|
|
33
|
+
const isBackground = (v) => backgroundSchema.safeParse(v).success;
|
|
34
|
+
function background(input, options) {
|
|
35
|
+
const action = buildPhotoAction(input, options, "background");
|
|
36
|
+
return { build: async () => backgroundSchema.parse({
|
|
37
|
+
type: "background",
|
|
38
|
+
__platform: "iMessage",
|
|
39
|
+
__fireAndForget: true,
|
|
40
|
+
action
|
|
41
|
+
}) };
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region ../imessage/src/content/contact-card.ts
|
|
45
|
+
/**
|
|
46
|
+
* iMessage-only "share contact card" control signal. Pushes the *local
|
|
47
|
+
* account's native contact card* (the name + photo a recipient sees in their
|
|
48
|
+
* Messages app) to a chat via the SDK's `chats.shareContactInfo`.
|
|
49
|
+
*
|
|
50
|
+
* This is Apple's "Share Name and Photo" mechanism — distinct from the
|
|
51
|
+
* universal `contact(...)` content, which uploads an arbitrary person's vCard
|
|
52
|
+
* as a *file* attachment. There is no payload: the card shared is always the
|
|
53
|
+
* bot account's own.
|
|
54
|
+
*
|
|
55
|
+
* Like `background`, it lives entirely under the iMessage provider and never
|
|
56
|
+
* enters the universal `Content` discriminated union. The framework recognizes
|
|
57
|
+
* it via two generic content-level contracts:
|
|
58
|
+
*
|
|
59
|
+
* 1. `__platform: "iMessage"` — `findUnsupportedPlatformContent` in
|
|
60
|
+
* `platform/build.ts` reads this tag and warns-and-skips when a different
|
|
61
|
+
* platform receives it.
|
|
62
|
+
* 2. `__fireAndForget: true` — `dispatchSend`'s fire-and-forget check treats
|
|
63
|
+
* this as a side-effecting send that returns no message id, the same way it
|
|
64
|
+
* treats `read` / `typing`.
|
|
65
|
+
*
|
|
66
|
+
* iMessage's `send` handler narrows back via the `isContactCard` type guard
|
|
67
|
+
* before dispatching to `chats.shareContactInfo`.
|
|
68
|
+
*/
|
|
69
|
+
const contactCardSchema = z.object({
|
|
70
|
+
type: z.literal("contactCard"),
|
|
71
|
+
__platform: z.literal("iMessage"),
|
|
72
|
+
__fireAndForget: z.literal(true)
|
|
73
|
+
});
|
|
74
|
+
const isContactCard = (v) => contactCardSchema.safeParse(v).success;
|
|
75
|
+
/**
|
|
76
|
+
* Share the bot account's native iMessage contact card (name + photo) with the
|
|
77
|
+
* chat. iMessage-only, remote-only.
|
|
78
|
+
*
|
|
79
|
+
* `space.send(nativeContactCard())` is the canonical form; `space.shareContactCard()`
|
|
80
|
+
* is sugar attached via `PlatformDef.space.actions` (only typed on
|
|
81
|
+
* `PlatformSpace<IMessageDef>`).
|
|
82
|
+
*
|
|
83
|
+
* This is an explicit, on-demand share and always fires — unlike the automatic
|
|
84
|
+
* best-effort share gated behind the `imessageSynced` project profile, which
|
|
85
|
+
* dedupes to once per chat per 24h (see `remote/contact-share.ts`). Works in
|
|
86
|
+
* both DMs and group chats; the recipient chooses whether to accept the card.
|
|
87
|
+
*
|
|
88
|
+
* `ContactCard` is intentionally not a member of the universal `Content`
|
|
89
|
+
* union — the `as unknown as Content` cast keeps the builder shape compatible
|
|
90
|
+
* with the framework's `ContentBuilder.build(): Promise<Content>` signature.
|
|
91
|
+
* The framework treats it as a fire-and-forget control signal at runtime.
|
|
92
|
+
*/
|
|
93
|
+
function nativeContactCard() {
|
|
94
|
+
return { build: async () => contactCardSchema.parse({
|
|
95
|
+
type: "contactCard",
|
|
96
|
+
__platform: "iMessage",
|
|
97
|
+
__fireAndForget: true
|
|
98
|
+
}) };
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region ../imessage/src/content/customized-mini-app.ts
|
|
102
|
+
const layoutSchema = appLayoutSchema;
|
|
103
|
+
/**
|
|
104
|
+
* iMessage-only mini-app card content. Lives entirely under the iMessage
|
|
105
|
+
* provider — never enters the universal `Content` discriminated union. The
|
|
106
|
+
* framework recognizes it via the generic content-level platform contract:
|
|
107
|
+
*
|
|
108
|
+
* - `__platform: "iMessage"` — `findUnsupportedPlatformContent` reads this tag
|
|
109
|
+
* and warns-and-skips when a different platform receives it.
|
|
110
|
+
*
|
|
111
|
+
* Unlike `background` / `read`, this content is **not** `__fireAndForget`: it
|
|
112
|
+
* produces a real outbound message, so the iMessage `send` handler narrows
|
|
113
|
+
* back to `CustomizedMiniApp` via the `isCustomizedMiniApp` guard and returns
|
|
114
|
+
* the resulting `ProviderMessageRecord` (rather than `void`).
|
|
115
|
+
*/
|
|
116
|
+
const customizedMiniAppSchema = z.object({
|
|
117
|
+
type: z.literal("customized-mini-app"),
|
|
118
|
+
__platform: z.literal("iMessage"),
|
|
119
|
+
appName: z.string().nonempty(),
|
|
120
|
+
appStoreId: z.number().int().positive().optional(),
|
|
121
|
+
extensionBundleId: z.string().nonempty(),
|
|
122
|
+
layout: layoutSchema,
|
|
123
|
+
live: z.boolean().optional(),
|
|
124
|
+
teamId: z.string(),
|
|
125
|
+
url: z.url()
|
|
126
|
+
});
|
|
127
|
+
const isCustomizedMiniApp = (v) => customizedMiniAppSchema.safeParse(v).success;
|
|
128
|
+
const asCustomizedMiniApp = (input) => customizedMiniAppSchema.parse({
|
|
129
|
+
type: "customized-mini-app",
|
|
130
|
+
__platform: "iMessage",
|
|
131
|
+
...input
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* Construct a `customized-mini-app` content value. iMessage-only, remote-only.
|
|
135
|
+
*
|
|
136
|
+
* The layout is what recipients see in the bubble. `teamId` and
|
|
137
|
+
* `extensionBundleId` identify the iMessage extension that receives `url` when
|
|
138
|
+
* the recipient taps the card; the server constructs the matching
|
|
139
|
+
* `MSMessageExtensionBalloonPlugin` plugin id from these values. `appStoreId`
|
|
140
|
+
* is optional and only points recipients without the extension at its App
|
|
141
|
+
* Store entry. `live` is optional; when omitted, the remote server keeps the
|
|
142
|
+
* static layout preview visible.
|
|
143
|
+
*
|
|
144
|
+
* `space.send(customizedMiniApp(...))` is the canonical form.
|
|
145
|
+
*
|
|
146
|
+
* `CustomizedMiniApp` is intentionally not a member of the universal `Content`
|
|
147
|
+
* union — the `as unknown as Content` cast keeps the builder shape compatible
|
|
148
|
+
* with the framework's `ContentBuilder.build(): Promise<Content>` signature.
|
|
149
|
+
*/
|
|
150
|
+
function customizedMiniApp(input) {
|
|
151
|
+
return { build: async () => asCustomizedMiniApp(input) };
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region ../imessage/src/content/effect.ts
|
|
155
|
+
const messageEffects = {
|
|
156
|
+
balloons: "com.apple.messages.effect.CKBalloonEffect",
|
|
157
|
+
celebration: "com.apple.messages.effect.CKHappyBirthdayEffect",
|
|
158
|
+
confetti: "com.apple.messages.effect.CKConfettiEffect",
|
|
159
|
+
echo: "com.apple.messages.effect.CKEchoEffect",
|
|
160
|
+
fireworks: "com.apple.messages.effect.CKFireworksEffect",
|
|
161
|
+
gentle: "com.apple.MobileSMS.expressivesend.gentle",
|
|
162
|
+
heart: "com.apple.messages.effect.CKHeartEffect",
|
|
163
|
+
invisible: "com.apple.MobileSMS.expressivesend.invisibleink",
|
|
164
|
+
lasers: "com.apple.messages.effect.CKLasersEffect",
|
|
165
|
+
loud: "com.apple.MobileSMS.expressivesend.loud",
|
|
166
|
+
slam: "com.apple.MobileSMS.expressivesend.impact",
|
|
167
|
+
sparkles: "com.apple.messages.effect.CKSparklesEffect",
|
|
168
|
+
spotlight: "com.apple.messages.effect.CKSpotlightEffect"
|
|
169
|
+
};
|
|
170
|
+
const SUPPORTED_EFFECTS = new Set(Object.values(messageEffects));
|
|
171
|
+
const resolveContent = (input) => typeof input === "string" ? text(input).build() : input.build();
|
|
172
|
+
function effect(input, messageEffect) {
|
|
173
|
+
return { build: async () => {
|
|
174
|
+
if (!SUPPORTED_EFFECTS.has(messageEffect)) throw new Error(`Unsupported iMessage message effect "${messageEffect}"`);
|
|
175
|
+
const inner = await resolveContent(input);
|
|
176
|
+
if (inner.type !== "text" && inner.type !== "markdown" && inner.type !== "attachment") throw new Error(`imessage effect() only supports text, markdown, and attachment content, got "${inner.type}"`);
|
|
177
|
+
return messageEffectSchema.parse({
|
|
178
|
+
type: "effect",
|
|
179
|
+
content: inner,
|
|
180
|
+
effect: messageEffect
|
|
181
|
+
});
|
|
182
|
+
} };
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/ids.ts
|
|
186
|
+
const dmChatGuid = (address) => `any;-;${address}`;
|
|
187
|
+
const chatTypeFromGuid = (guid) => guid.includes(";+;") ? "group" : "dm";
|
|
188
|
+
const addTextPart = (parts, text) => {
|
|
189
|
+
const trimmed = text?.trim();
|
|
190
|
+
if (trimmed) parts.push({
|
|
191
|
+
type: "text",
|
|
192
|
+
text: trimmed
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const hasUsableTextPart = (text) => text?.split("").some((segment) => segment.trim()) ?? false;
|
|
196
|
+
const addAttachmentParts = (parts, attachments) => {
|
|
197
|
+
for (const attachment of attachments) if (attachment) parts.push({
|
|
198
|
+
type: "attachment",
|
|
199
|
+
attachment
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const toOrderedParts = (text, attachments) => {
|
|
203
|
+
const parts = [];
|
|
204
|
+
if (!text) {
|
|
205
|
+
addAttachmentParts(parts, attachments);
|
|
206
|
+
return parts;
|
|
207
|
+
}
|
|
208
|
+
if (!text.includes("")) {
|
|
209
|
+
addAttachmentParts(parts, attachments);
|
|
210
|
+
addTextPart(parts, text);
|
|
211
|
+
return parts;
|
|
212
|
+
}
|
|
213
|
+
const textSegments = text.split("");
|
|
214
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
215
|
+
addTextPart(parts, textSegments[i]);
|
|
216
|
+
const attachment = attachments[i];
|
|
217
|
+
if (attachment) parts.push({
|
|
218
|
+
type: "attachment",
|
|
219
|
+
attachment
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
addTextPart(parts, textSegments.slice(attachments.length).join(""));
|
|
223
|
+
return parts;
|
|
224
|
+
};
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region ../imessage/src/shared/vcard.ts
|
|
227
|
+
const VCARD_MIME_TYPES = new Set([
|
|
228
|
+
"text/vcard",
|
|
229
|
+
"text/x-vcard",
|
|
230
|
+
"text/directory",
|
|
231
|
+
"application/vcard",
|
|
232
|
+
"application/x-vcard"
|
|
233
|
+
]);
|
|
234
|
+
const normalizeMimeType = (mimeType) => (mimeType.split(";")[0] ?? "").trim().toLowerCase();
|
|
235
|
+
const isVCardAttachment = (mimeType, fileName) => {
|
|
236
|
+
if (mimeType && VCARD_MIME_TYPES.has(normalizeMimeType(mimeType))) return true;
|
|
237
|
+
return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
|
|
238
|
+
};
|
|
239
|
+
const vcardFileName = (contact) => {
|
|
240
|
+
return `${(contact.name?.formatted ?? contact.user?.id ?? "contact").replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
|
|
241
|
+
};
|
|
242
|
+
const readLocalAttachment = async (att) => {
|
|
243
|
+
if (!att.localPath) throw new Error(`iMessage attachment ${att.id} has no local file available on disk`);
|
|
244
|
+
return readFile(att.localPath);
|
|
245
|
+
};
|
|
246
|
+
const toAttachmentContent = (att) => {
|
|
247
|
+
const { localPath } = att;
|
|
248
|
+
return asAttachment({
|
|
249
|
+
id: att.id,
|
|
250
|
+
name: att.fileName ?? "attachment",
|
|
251
|
+
mimeType: att.mimeType,
|
|
252
|
+
size: att.sizeBytes,
|
|
253
|
+
read: () => readLocalAttachment(att),
|
|
254
|
+
stream: localPath ? async () => Readable.toWeb(createReadStream(localPath)) : void 0
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
const toVCardContent = async (att) => {
|
|
258
|
+
try {
|
|
259
|
+
return asContact(fromVCard((await readLocalAttachment(att)).toString("utf8")));
|
|
260
|
+
} catch {
|
|
261
|
+
return toAttachmentContent(att);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att);
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/local/inbound.ts
|
|
267
|
+
const ATTACHMENT_JOIN_RETRY_DELAY_MS = 250;
|
|
268
|
+
const ATTACHMENT_JOIN_RETRY_LIMIT = 8;
|
|
269
|
+
const ATTACHMENT_JOIN_FETCH_LIMIT = 10;
|
|
270
|
+
const hasAttachmentPlaceholder = (message) => message.text?.includes("") ?? false;
|
|
271
|
+
const isPendingAttachmentJoin = (message) => message.attachments.length === 0 && (message.hasAttachments || hasAttachmentPlaceholder(message));
|
|
272
|
+
const replyTargetId = (message) => message.threadRootMessageId ?? void 0;
|
|
273
|
+
const stubReplyTarget = (space, targetId) => ({
|
|
274
|
+
id: targetId,
|
|
275
|
+
content: asCustom({
|
|
276
|
+
imessage_type: "reply-target",
|
|
277
|
+
stub: true
|
|
278
|
+
}),
|
|
279
|
+
space
|
|
280
|
+
});
|
|
281
|
+
const asProviderReply = (content, target) => asReply({
|
|
282
|
+
content,
|
|
283
|
+
target
|
|
284
|
+
});
|
|
285
|
+
const wrapReply = (messages, targetId) => {
|
|
286
|
+
if (!targetId) return messages;
|
|
287
|
+
return messages.map((message) => ({
|
|
288
|
+
...message,
|
|
289
|
+
content: asProviderReply(message.content, stubReplyTarget(message.space, targetId))
|
|
290
|
+
}));
|
|
291
|
+
};
|
|
292
|
+
const refetchUntilAttachmentsSettle = async (client, message) => {
|
|
293
|
+
if (!message.chatId) return message;
|
|
294
|
+
for (let attempt = 0; attempt < ATTACHMENT_JOIN_RETRY_LIMIT; attempt += 1) {
|
|
295
|
+
await setTimeout(ATTACHMENT_JOIN_RETRY_DELAY_MS);
|
|
296
|
+
let rows;
|
|
297
|
+
try {
|
|
298
|
+
rows = await client.getMessages({
|
|
299
|
+
chatId: message.chatId,
|
|
300
|
+
limit: ATTACHMENT_JOIN_FETCH_LIMIT,
|
|
301
|
+
since: message.createdAt
|
|
302
|
+
});
|
|
303
|
+
} catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const refreshed = rows.find((row) => row.id === message.id);
|
|
307
|
+
if (refreshed && !isPendingAttachmentJoin(refreshed)) return refreshed;
|
|
308
|
+
}
|
|
309
|
+
return message;
|
|
310
|
+
};
|
|
311
|
+
const toMessages = async (message) => {
|
|
312
|
+
const { chatId, chatKind } = message;
|
|
313
|
+
if (!chatId || chatKind === "unknown") return [];
|
|
314
|
+
if (message.reaction !== null || message.kind !== "text" || message.retractedAt !== null) return [];
|
|
315
|
+
if (isPendingAttachmentJoin(message)) return [];
|
|
316
|
+
const base = {
|
|
317
|
+
sender: {
|
|
318
|
+
id: message.participant ?? "",
|
|
319
|
+
...message.participant ? { address: message.participant } : {}
|
|
320
|
+
},
|
|
321
|
+
space: {
|
|
322
|
+
id: chatId,
|
|
323
|
+
type: chatKind === "group" ? "group" : "dm",
|
|
324
|
+
phone: ""
|
|
325
|
+
},
|
|
326
|
+
timestamp: message.createdAt
|
|
327
|
+
};
|
|
328
|
+
const targetId = replyTargetId(message);
|
|
329
|
+
if (message.attachments.length > 0) {
|
|
330
|
+
if (!hasUsableTextPart(message.text)) return wrapReply(await Promise.all(message.attachments.map(async (att) => ({
|
|
331
|
+
...base,
|
|
332
|
+
id: `${message.id}:${att.id}`,
|
|
333
|
+
content: await localAttachmentContent(att)
|
|
334
|
+
}))), targetId);
|
|
335
|
+
const parts = toOrderedParts(message.text, message.attachments);
|
|
336
|
+
const messages = [];
|
|
337
|
+
for (let i = 0; i < parts.length; i++) {
|
|
338
|
+
const part = parts[i];
|
|
339
|
+
if (!part) continue;
|
|
340
|
+
messages.push(part.type === "text" ? {
|
|
341
|
+
...base,
|
|
342
|
+
id: `${message.id}:text:${i}`,
|
|
343
|
+
content: asText(part.text),
|
|
344
|
+
partIndex: i
|
|
345
|
+
} : {
|
|
346
|
+
...base,
|
|
347
|
+
id: `${message.id}:${part.attachment.id}`,
|
|
348
|
+
content: await localAttachmentContent(part.attachment),
|
|
349
|
+
partIndex: i
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return wrapReply(messages, targetId);
|
|
353
|
+
}
|
|
354
|
+
return wrapReply([{
|
|
355
|
+
...base,
|
|
356
|
+
id: message.id,
|
|
357
|
+
content: {
|
|
358
|
+
type: "text",
|
|
359
|
+
text: message.text ?? ""
|
|
360
|
+
}
|
|
361
|
+
}], targetId);
|
|
362
|
+
};
|
|
363
|
+
const messages$1 = (client) => stream((emit, end) => {
|
|
364
|
+
let lastPromise = Promise.resolve();
|
|
365
|
+
const handleIncoming = async (message) => {
|
|
366
|
+
const ms = await toMessages(isPendingAttachmentJoin(message) ? await refetchUntilAttachmentsSettle(client, message) : message);
|
|
367
|
+
for (const m of ms) await emit(m);
|
|
368
|
+
};
|
|
369
|
+
const startPromise = client.startWatching({
|
|
370
|
+
onIncomingMessage: (message) => {
|
|
371
|
+
lastPromise = lastPromise.then(() => handleIncoming(message)).catch(end);
|
|
372
|
+
},
|
|
373
|
+
onError: end
|
|
374
|
+
}).catch(end);
|
|
375
|
+
return async () => {
|
|
376
|
+
await startPromise.catch(() => {});
|
|
377
|
+
await client.stopWatching();
|
|
378
|
+
await lastPromise.catch(() => {});
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region ../imessage/src/shared/errors.ts
|
|
383
|
+
const LOCAL_IMESSAGE_PLATFORM = "iMessage (local mode)";
|
|
384
|
+
const unsupportedLocalContent = (type, detail) => UnsupportedError.content(type, LOCAL_IMESSAGE_PLATFORM, detail);
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/local/send.ts
|
|
387
|
+
const synthRecord = (spaceId, content) => ({
|
|
388
|
+
id: crypto.randomUUID(),
|
|
389
|
+
content,
|
|
390
|
+
space: { id: spaceId },
|
|
391
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
392
|
+
});
|
|
393
|
+
const sendTempFile = async (client, spaceId, name, data) => {
|
|
394
|
+
const safeName = basename(name) || "attachment";
|
|
395
|
+
const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
|
|
396
|
+
const tmp = join(dir, safeName);
|
|
397
|
+
await writeFile(tmp, data);
|
|
398
|
+
try {
|
|
399
|
+
await client.send({
|
|
400
|
+
to: spaceId,
|
|
401
|
+
attachments: [tmp]
|
|
402
|
+
});
|
|
403
|
+
} finally {
|
|
404
|
+
await rm(dir, {
|
|
405
|
+
recursive: true,
|
|
406
|
+
force: true
|
|
407
|
+
}).catch(() => {});
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const send$1 = async (client, spaceId, content) => {
|
|
411
|
+
switch (content.type) {
|
|
412
|
+
case "text":
|
|
413
|
+
await client.send({
|
|
414
|
+
to: spaceId,
|
|
415
|
+
text: content.text
|
|
416
|
+
});
|
|
417
|
+
return synthRecord(spaceId, content);
|
|
418
|
+
case "attachment":
|
|
419
|
+
await sendTempFile(client, spaceId, content.name, await content.read());
|
|
420
|
+
return synthRecord(spaceId, content);
|
|
421
|
+
case "contact": {
|
|
422
|
+
const vcf = await toVCard(content);
|
|
423
|
+
await sendTempFile(client, spaceId, vcardFileName(content), Buffer.from(vcf, "utf8"));
|
|
424
|
+
return synthRecord(spaceId, content);
|
|
425
|
+
}
|
|
426
|
+
case "effect": throw unsupportedLocalContent("effect", "message effects require remote iMessage");
|
|
427
|
+
case "poll": throw unsupportedLocalContent("poll");
|
|
428
|
+
default: throw unsupportedLocalContent(content.type);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const getMessage$1 = async (_client, _id) => void 0;
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/local/api.ts
|
|
434
|
+
const messages = (client) => messages$1(client);
|
|
435
|
+
const send = (client, spaceId, content) => send$1(client, spaceId, content);
|
|
436
|
+
const getMessage = (client, id) => getMessage$1(client, id);
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/types.ts
|
|
439
|
+
const configSchema = z.object({});
|
|
440
|
+
const userSchema = z.object({
|
|
441
|
+
address: z.string().optional(),
|
|
442
|
+
country: z.string().optional(),
|
|
443
|
+
service: z.enum([
|
|
444
|
+
"iMessage",
|
|
445
|
+
"SMS",
|
|
446
|
+
"RCS",
|
|
447
|
+
"unknown"
|
|
448
|
+
]).optional()
|
|
449
|
+
});
|
|
450
|
+
const spaceSchema = z.object({
|
|
451
|
+
id: z.string(),
|
|
452
|
+
type: z.enum(["dm", "group"]),
|
|
453
|
+
phone: z.string()
|
|
454
|
+
});
|
|
455
|
+
const spaceParamsSchema = z.object({});
|
|
456
|
+
const messageSchema = z.object({
|
|
457
|
+
partIndex: z.number().int().nonnegative().optional(),
|
|
458
|
+
parentId: z.string().optional()
|
|
459
|
+
});
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/index.ts
|
|
462
|
+
const LOCAL_PLATFORM = "iMessage (local mode)";
|
|
463
|
+
const unsupportedAction = (action, detail) => {
|
|
464
|
+
throw UnsupportedError.action(action, LOCAL_PLATFORM, detail);
|
|
465
|
+
};
|
|
466
|
+
const handleLocalOnlySend = async (client, spaceId, content) => {
|
|
467
|
+
if (content.type === "typing") return;
|
|
468
|
+
if (content.type === "app") return await send(client, spaceId, await text(await content.url()).build());
|
|
469
|
+
if (content.type === "edit") return unsupportedAction("edit");
|
|
470
|
+
if (content.type === "unsend") return unsupportedAction("unsend");
|
|
471
|
+
if (content.type === "streamText") return unsupportedAction("streamText", "streaming text responses require remote iMessage");
|
|
472
|
+
if (content.type === "rename") return unsupportedAction("rename", "renaming chats requires remote iMessage");
|
|
473
|
+
if (content.type === "avatar") return unsupportedAction("avatar", "setting group avatars requires remote iMessage");
|
|
474
|
+
if (content.type === "addMember") return unsupportedAction("addMember", "adding members requires remote iMessage");
|
|
475
|
+
if (content.type === "removeMember") return unsupportedAction("removeMember", "removing members requires remote iMessage");
|
|
476
|
+
if (content.type === "leaveSpace") return unsupportedAction("leaveSpace", "leaving chats requires remote iMessage");
|
|
477
|
+
if (content.type === "read") return unsupportedAction("read", "marking chats as read requires remote iMessage");
|
|
478
|
+
if (content.type === "reply") return unsupportedAction("reply");
|
|
479
|
+
if (content.type === "reaction") return unsupportedAction("react");
|
|
480
|
+
if (isBackground(content)) return unsupportedAction("background", "chat backgrounds require remote iMessage");
|
|
481
|
+
if (isContactCard(content)) return unsupportedAction("shareContactCard", "sharing the contact card requires remote iMessage");
|
|
482
|
+
if (isCustomizedMiniApp(content)) return unsupportedAction("customized-mini-app", "mini app cards require remote iMessage");
|
|
483
|
+
return await send(client, spaceId, content);
|
|
484
|
+
};
|
|
485
|
+
const imessage = definePlatform("iMessage", {
|
|
486
|
+
config: configSchema,
|
|
487
|
+
static: { effect: { message: messageEffects } },
|
|
488
|
+
lifecycle: {
|
|
489
|
+
createClient: async () => new IMessageSDK(),
|
|
490
|
+
destroyClient: async ({ client }) => {
|
|
491
|
+
await client.close();
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
user: {
|
|
495
|
+
schema: userSchema,
|
|
496
|
+
resolve: async ({ input }) => ({ id: input.userID })
|
|
497
|
+
},
|
|
498
|
+
space: {
|
|
499
|
+
schema: spaceSchema,
|
|
500
|
+
params: spaceParamsSchema,
|
|
501
|
+
create: async ({ input }) => {
|
|
502
|
+
if (input.users.length === 0) throw new Error("iMessage space creation requires at least one user");
|
|
503
|
+
if (input.users.length > 1) return unsupportedAction("space.create", "local mode cannot create group chats — use space.get(chatGuid) for an existing group");
|
|
504
|
+
return {
|
|
505
|
+
id: dmChatGuid(input.users[0]?.id ?? ""),
|
|
506
|
+
type: "dm",
|
|
507
|
+
phone: ""
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
get: async ({ input }) => ({
|
|
511
|
+
id: input.id,
|
|
512
|
+
type: chatTypeFromGuid(input.id),
|
|
513
|
+
phone: ""
|
|
514
|
+
}),
|
|
515
|
+
actions: {
|
|
516
|
+
background: async (space, input, options) => {
|
|
517
|
+
await space.send(background(input, options));
|
|
518
|
+
},
|
|
519
|
+
shareContactCard: async (space) => {
|
|
520
|
+
await space.send(nativeContactCard());
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
message: { schema: messageSchema },
|
|
525
|
+
messages: ({ client }) => messages(client),
|
|
526
|
+
send: async ({ space, content, client }) => handleLocalOnlySend(client, space.id, content),
|
|
527
|
+
actions: {
|
|
528
|
+
getMessage: async ({ client }, _space, messageId) => getMessage(client, messageId),
|
|
529
|
+
getMembers: async () => unsupportedAction("getMembers", "listing members requires remote iMessage"),
|
|
530
|
+
getAvatar: async () => unsupportedAction("getAvatar", "fetching group avatars requires remote iMessage"),
|
|
531
|
+
getDisplayName: async () => unsupportedAction("getDisplayName", "reading chat display names requires remote iMessage"),
|
|
532
|
+
getAttachment: async () => unsupportedAction("getAttachment", "fetching attachments by GUID requires remote iMessage")
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
//#endregion
|
|
536
|
+
export { background, customizedMiniApp, effect, imessage, nativeContactCard, read };
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spectrum-ts/imessage-local",
|
|
3
|
+
"version": "10.0.0",
|
|
4
|
+
"description": "Local macOS iMessage provider for spectrum-ts.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/photon-hq/spectrum-ts.git",
|
|
8
|
+
"directory": "packages/imessage-local"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://photon.codes/spectrum",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/photon-hq/spectrum-ts/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@photon-ai/imessage-kit": "^3.0.0",
|
|
30
|
+
"zod": "^4.2.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@spectrum-ts/core": "^10.0.0",
|
|
34
|
+
"typescript": "^5 || ^6.0.0"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|