spectrum-ts 0.0.1 → 0.1.2
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/dist/chunk-3TBRO2J7.js +58 -0
- package/dist/chunk-UZ2CXPOD.js +175 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +348 -0
- package/dist/providers/imessage/index.d.ts +82 -0
- package/dist/providers/imessage/index.js +418 -0
- package/dist/providers/terminal/index.d.ts +35 -0
- package/dist/providers/terminal/index.js +61 -0
- package/dist/stream-DGy4geUK.d.ts +8 -0
- package/dist/types-eXHZpal1.d.ts +261 -0
- package/package.json +48 -3
- package/src/index.ts +26 -0
- package/src/platform/define.ts +309 -0
- package/src/platform/types.ts +438 -0
- package/src/providers/imessage/auth.ts +161 -0
- package/src/providers/imessage/index.ts +153 -0
- package/src/providers/imessage/local.ts +55 -0
- package/src/providers/imessage/remote.ts +157 -0
- package/src/providers/imessage/types.ts +31 -0
- package/src/providers/terminal/index.ts +66 -0
- package/src/spectrum.ts +390 -0
- package/src/types/content.ts +85 -0
- package/src/types/message.ts +18 -0
- package/src/types/space.ts +10 -0
- package/src/types/user.ts +4 -0
- package/src/utils/cloud.ts +1 -0
- package/src/utils/stream.ts +71 -0
- package/index.js +0 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createClient, directChat } from "@photon-ai/advanced-imessage";
|
|
2
|
+
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
3
|
+
import { definePlatform } from "../../platform/define";
|
|
4
|
+
import { createCloudClients, disposeCloudAuth } from "./auth";
|
|
5
|
+
import { messages as localMessages, send as localSend } from "./local";
|
|
6
|
+
import {
|
|
7
|
+
messages as remoteMessages,
|
|
8
|
+
reactToMessage as remoteReactToMessage,
|
|
9
|
+
replyToMessage as remoteReplyToMessage,
|
|
10
|
+
send as remoteSend,
|
|
11
|
+
startTyping as remoteStartTyping,
|
|
12
|
+
stopTyping as remoteStopTyping,
|
|
13
|
+
} from "./remote";
|
|
14
|
+
import {
|
|
15
|
+
configSchema,
|
|
16
|
+
type IMessageClient,
|
|
17
|
+
isLocal,
|
|
18
|
+
spaceSchema,
|
|
19
|
+
} from "./types";
|
|
20
|
+
|
|
21
|
+
export const imessage = definePlatform("iMessage", {
|
|
22
|
+
config: configSchema,
|
|
23
|
+
|
|
24
|
+
static: {
|
|
25
|
+
tapbacks: {
|
|
26
|
+
love: "love",
|
|
27
|
+
like: "like",
|
|
28
|
+
dislike: "dislike",
|
|
29
|
+
laugh: "laugh",
|
|
30
|
+
emphasize: "emphasize",
|
|
31
|
+
question: "question",
|
|
32
|
+
} as const,
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
user: {
|
|
36
|
+
resolve: async ({ input }) => ({ id: input.userID }),
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
space: {
|
|
40
|
+
schema: spaceSchema,
|
|
41
|
+
resolve: async ({ input, client }) => {
|
|
42
|
+
if (isLocal(client)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Space creation is not supported in local mode. Local mode only supports replying to messages."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (input.users.length === 0) {
|
|
49
|
+
throw new Error("iMessage space creation requires at least one user");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const addresses = input.users.map((u) => u.id);
|
|
53
|
+
|
|
54
|
+
if (input.users.length === 1) {
|
|
55
|
+
return {
|
|
56
|
+
id: directChat(addresses[0] ?? "") as string,
|
|
57
|
+
type: "dm" as const,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const remote = client[0];
|
|
62
|
+
if (!remote) {
|
|
63
|
+
throw new Error("No remote iMessage client available");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { chat } = await remote.chats.create(addresses);
|
|
67
|
+
return { id: chat.guid as string, type: "group" as const };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
lifecycle: {
|
|
72
|
+
createClient: async ({
|
|
73
|
+
config,
|
|
74
|
+
projectId,
|
|
75
|
+
projectSecret,
|
|
76
|
+
}): Promise<IMessageClient> => {
|
|
77
|
+
if (config.local) {
|
|
78
|
+
return new IMessageSDK();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (config.clients) {
|
|
82
|
+
const entries = Array.isArray(config.clients)
|
|
83
|
+
? config.clients
|
|
84
|
+
: [config.clients];
|
|
85
|
+
return entries.map((e) =>
|
|
86
|
+
createClient({ address: e.address, tls: true, token: e.token })
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!(projectId && projectSecret)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"iMessage requires projectId and projectSecret. " +
|
|
93
|
+
"Either pass credentials to Spectrum(), use local mode: imessage.config({ local: true }), " +
|
|
94
|
+
"or provide explicit client config: imessage.config({ clients: [...] })"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return await createCloudClients(projectId, projectSecret);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
destroyClient: async ({ client }: { client: IMessageClient }) => {
|
|
102
|
+
if (isLocal(client)) {
|
|
103
|
+
await client.close();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await disposeCloudAuth(client);
|
|
107
|
+
await Promise.all(client.map((c) => c.close()));
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
events: {
|
|
112
|
+
messages: ({ client }) =>
|
|
113
|
+
isLocal(client) ? localMessages(client) : remoteMessages(client),
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
actions: {
|
|
117
|
+
send: async ({ space, content, client }) => {
|
|
118
|
+
for (const item of content) {
|
|
119
|
+
if (isLocal(client)) {
|
|
120
|
+
await localSend(client, space.id, item);
|
|
121
|
+
} else {
|
|
122
|
+
await remoteSend(client, space.id, item);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
startTyping: async ({ space, client }) => {
|
|
127
|
+
if (isLocal(client)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await remoteStartTyping(client, space.id);
|
|
131
|
+
},
|
|
132
|
+
stopTyping: async ({ space, client }) => {
|
|
133
|
+
if (isLocal(client)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await remoteStopTyping(client, space.id);
|
|
137
|
+
},
|
|
138
|
+
reactToMessage: async ({ space, messageId, reaction, client }) => {
|
|
139
|
+
if (isLocal(client)) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await remoteReactToMessage(client, space.id, messageId, reaction);
|
|
143
|
+
},
|
|
144
|
+
replyToMessage: async ({ space, messageId, content, client }) => {
|
|
145
|
+
if (isLocal(client)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const item of content) {
|
|
149
|
+
await remoteReplyToMessage(client, space.id, messageId, item);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
IMessageSDK,
|
|
6
|
+
Message as LocalIMessage,
|
|
7
|
+
} from "@photon-ai/imessage-kit";
|
|
8
|
+
import type { Content } from "../../types/content";
|
|
9
|
+
import { type ManagedStream, stream } from "../../utils/stream";
|
|
10
|
+
import type { IMessageMessage } from "./types";
|
|
11
|
+
|
|
12
|
+
const toSpace = (message: LocalIMessage): IMessageMessage["space"] => ({
|
|
13
|
+
id: `${message.isGroupChat ? "any;+;" : "any;-;"}${message.chatId}`,
|
|
14
|
+
type: message.isGroupChat ? "group" : "dm",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const toMessage = (message: LocalIMessage): IMessageMessage => ({
|
|
18
|
+
id: message.guid,
|
|
19
|
+
content: [{ type: "plain_text", text: message.text ?? "" }],
|
|
20
|
+
sender: { id: message.sender ?? "" },
|
|
21
|
+
space: toSpace(message),
|
|
22
|
+
timestamp: message.date ?? new Date(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const messages = (client: IMessageSDK): ManagedStream<IMessageMessage> =>
|
|
26
|
+
stream((emit) => {
|
|
27
|
+
client.startWatching({
|
|
28
|
+
onMessage: (message) => emit(toMessage(message)),
|
|
29
|
+
});
|
|
30
|
+
return () => client.stopWatching();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const send = async (
|
|
34
|
+
client: IMessageSDK,
|
|
35
|
+
spaceId: string,
|
|
36
|
+
content: Content
|
|
37
|
+
) => {
|
|
38
|
+
switch (content.type) {
|
|
39
|
+
case "plain_text":
|
|
40
|
+
await client.send(spaceId, content.text);
|
|
41
|
+
break;
|
|
42
|
+
case "attachment": {
|
|
43
|
+
const tmp = join(tmpdir(), `spectrum-${Date.now()}-${content.name}`);
|
|
44
|
+
await writeFile(tmp, content.data);
|
|
45
|
+
try {
|
|
46
|
+
await client.send(spaceId, { files: [tmp] });
|
|
47
|
+
} finally {
|
|
48
|
+
await unlink(tmp).catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
default:
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdvancedIMessage,
|
|
3
|
+
chatGuid,
|
|
4
|
+
type MessageEvent,
|
|
5
|
+
messageGuid,
|
|
6
|
+
Reaction,
|
|
7
|
+
} from "@photon-ai/advanced-imessage";
|
|
8
|
+
import type { Content } from "../../types/content";
|
|
9
|
+
import { type ManagedStream, mergeStreams, stream } from "../../utils/stream";
|
|
10
|
+
import type { IMessageMessage } from "./types";
|
|
11
|
+
|
|
12
|
+
type ReceivedEvent = Extract<MessageEvent, { type: "message.received" }>;
|
|
13
|
+
|
|
14
|
+
const TAPBACK_NAMES: ReadonlySet<string> = new Set(
|
|
15
|
+
Object.values(Reaction).filter((r) => r !== "emoji" && r !== "sticker")
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const toMessage = (event: ReceivedEvent): IMessageMessage => ({
|
|
19
|
+
id: event.message.guid as string,
|
|
20
|
+
content: [{ type: "plain_text", text: event.message.text ?? "" }],
|
|
21
|
+
sender: { id: event.message.sender?.address ?? "" },
|
|
22
|
+
space: {
|
|
23
|
+
id: event.chatGuid,
|
|
24
|
+
type: event.chatGuid.includes(";+;") ? "group" : "dm",
|
|
25
|
+
},
|
|
26
|
+
timestamp: event.timestamp,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const clientStream = (
|
|
30
|
+
client: AdvancedIMessage
|
|
31
|
+
): ManagedStream<IMessageMessage> => {
|
|
32
|
+
const sub = client.messages.subscribe("message.received");
|
|
33
|
+
return stream<IMessageMessage>((emit, end) => {
|
|
34
|
+
(async () => {
|
|
35
|
+
try {
|
|
36
|
+
for await (const event of sub) {
|
|
37
|
+
emit(toMessage(event));
|
|
38
|
+
}
|
|
39
|
+
end();
|
|
40
|
+
} catch (e) {
|
|
41
|
+
end(e);
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
return () => sub.close();
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const messages = (
|
|
49
|
+
clients: AdvancedIMessage[]
|
|
50
|
+
): ManagedStream<IMessageMessage> => mergeStreams(clients.map(clientStream));
|
|
51
|
+
|
|
52
|
+
export const startTyping = async (
|
|
53
|
+
clients: AdvancedIMessage[],
|
|
54
|
+
spaceId: string
|
|
55
|
+
) => {
|
|
56
|
+
const remote = clients[0];
|
|
57
|
+
if (!remote) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await remote.chats.startTyping(chatGuid(spaceId));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const stopTyping = async (
|
|
64
|
+
clients: AdvancedIMessage[],
|
|
65
|
+
spaceId: string
|
|
66
|
+
) => {
|
|
67
|
+
const remote = clients[0];
|
|
68
|
+
if (!remote) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await remote.chats.stopTyping(chatGuid(spaceId));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const send = async (
|
|
75
|
+
clients: AdvancedIMessage[],
|
|
76
|
+
spaceId: string,
|
|
77
|
+
content: Content
|
|
78
|
+
) => {
|
|
79
|
+
const remote = clients[0];
|
|
80
|
+
if (!remote) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
switch (content.type) {
|
|
84
|
+
case "plain_text":
|
|
85
|
+
await remote.messages.send(chatGuid(spaceId), content.text);
|
|
86
|
+
break;
|
|
87
|
+
case "attachment": {
|
|
88
|
+
const attachment = await remote.attachments.upload({
|
|
89
|
+
data: content.data,
|
|
90
|
+
fileName: content.name,
|
|
91
|
+
mimeType: content.mimeType,
|
|
92
|
+
});
|
|
93
|
+
await remote.messages.send(chatGuid(spaceId), "", {
|
|
94
|
+
attachment: attachment.guid,
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const replyToMessage = async (
|
|
104
|
+
clients: AdvancedIMessage[],
|
|
105
|
+
spaceId: string,
|
|
106
|
+
msgId: string,
|
|
107
|
+
content: Content
|
|
108
|
+
) => {
|
|
109
|
+
const remote = clients[0];
|
|
110
|
+
if (!remote) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const chat = chatGuid(spaceId);
|
|
115
|
+
const replyTo = messageGuid(msgId);
|
|
116
|
+
|
|
117
|
+
switch (content.type) {
|
|
118
|
+
case "plain_text":
|
|
119
|
+
await remote.messages.send(chat, content.text, { replyTo });
|
|
120
|
+
break;
|
|
121
|
+
case "attachment": {
|
|
122
|
+
const attachment = await remote.attachments.upload({
|
|
123
|
+
data: content.data,
|
|
124
|
+
fileName: content.name,
|
|
125
|
+
mimeType: content.mimeType,
|
|
126
|
+
});
|
|
127
|
+
await remote.messages.send(chat, "", {
|
|
128
|
+
attachment: attachment.guid,
|
|
129
|
+
replyTo,
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
default:
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const reactToMessage = async (
|
|
139
|
+
clients: AdvancedIMessage[],
|
|
140
|
+
spaceId: string,
|
|
141
|
+
msgId: string,
|
|
142
|
+
reaction: string
|
|
143
|
+
) => {
|
|
144
|
+
const remote = clients[0];
|
|
145
|
+
if (!remote) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const chat = chatGuid(spaceId);
|
|
150
|
+
const msg = messageGuid(msgId);
|
|
151
|
+
|
|
152
|
+
if (TAPBACK_NAMES.has(reaction)) {
|
|
153
|
+
await remote.messages.react(chat, msg, reaction as Reaction);
|
|
154
|
+
} else {
|
|
155
|
+
await remote.messages.reactEmoji(chat, msg, reaction);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AdvancedIMessage } from "@photon-ai/advanced-imessage";
|
|
2
|
+
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import type { SchemaMessage } from "../../platform/types";
|
|
5
|
+
|
|
6
|
+
export type IMessageClient = IMessageSDK | AdvancedIMessage[];
|
|
7
|
+
|
|
8
|
+
export const isLocal = (client: IMessageClient): client is IMessageSDK =>
|
|
9
|
+
client instanceof IMessageSDK;
|
|
10
|
+
|
|
11
|
+
const clientEntry = z.object({ address: z.string(), token: z.string() });
|
|
12
|
+
|
|
13
|
+
export const configSchema = z.union([
|
|
14
|
+
z.object({ local: z.literal(true) }),
|
|
15
|
+
z.object({
|
|
16
|
+
local: z.literal(false).optional().default(false),
|
|
17
|
+
clients: clientEntry.or(z.array(clientEntry)).optional(),
|
|
18
|
+
}),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const userSchema = z.object({});
|
|
22
|
+
|
|
23
|
+
export const spaceSchema = z.object({
|
|
24
|
+
id: z.string(),
|
|
25
|
+
type: z.enum(["dm", "group"]),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type IMessageMessage = SchemaMessage<
|
|
29
|
+
typeof userSchema,
|
|
30
|
+
typeof spaceSchema
|
|
31
|
+
>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { definePlatform } from "../../platform/define";
|
|
4
|
+
|
|
5
|
+
export const terminal = definePlatform("terminal", {
|
|
6
|
+
config: z.object({}),
|
|
7
|
+
|
|
8
|
+
user: {
|
|
9
|
+
resolve: async ({ input }) => ({
|
|
10
|
+
id: input.userID,
|
|
11
|
+
}),
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
space: {
|
|
15
|
+
resolve: async () => ({
|
|
16
|
+
id: "terminal",
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
lifecycle: {
|
|
21
|
+
createClient: async () => {
|
|
22
|
+
const client = createInterface({
|
|
23
|
+
input: process.stdin,
|
|
24
|
+
output: process.stdout,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
client.on("SIGINT", () => {
|
|
28
|
+
client.close();
|
|
29
|
+
process.kill(process.pid, "SIGINT");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return client;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
destroyClient: async ({ client }) => {
|
|
36
|
+
client.close();
|
|
37
|
+
process.stdin.unref();
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
events: {
|
|
42
|
+
async *messages({ client }) {
|
|
43
|
+
for await (const line of client) {
|
|
44
|
+
yield {
|
|
45
|
+
id: crypto.randomUUID(),
|
|
46
|
+
content: [{ type: "plain_text" as const, text: line }],
|
|
47
|
+
sender: { id: "terminal-user" },
|
|
48
|
+
space: { id: "terminal" },
|
|
49
|
+
timestamp: new Date(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
actions: {
|
|
56
|
+
send: async ({ content }) => {
|
|
57
|
+
const outputs = content
|
|
58
|
+
.filter((c) => c.type === "plain_text")
|
|
59
|
+
.map((c) => c.text);
|
|
60
|
+
|
|
61
|
+
for (const output of outputs) {
|
|
62
|
+
console.log(output);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|