spectrum-ts 0.2.2 → 0.4.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.
@@ -1,55 +0,0 @@
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
- };
@@ -1,157 +0,0 @@
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
- };
@@ -1,31 +0,0 @@
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
- >;
@@ -1,66 +0,0 @@
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
- });
@@ -1,77 +0,0 @@
1
- import {
2
- createClient,
3
- type WhatsAppClient,
4
- } from "@photon-ai/whatsapp-business";
5
- import { definePlatform } from "../../platform/define";
6
- import { messages, reactToMessage, replyToMessage, send } from "./messages";
7
- import { configSchema, spaceSchema } from "./types";
8
-
9
- export const whatsappBusiness = definePlatform("WhatsApp Business", {
10
- config: configSchema,
11
-
12
- user: {
13
- resolve: async ({ input }) => ({ id: input.userID }),
14
- },
15
-
16
- space: {
17
- schema: spaceSchema,
18
- resolve: async ({ input }) => {
19
- if (input.users.length === 0) {
20
- throw new Error("WhatsApp space creation requires at least one user");
21
- }
22
- if (input.users.length > 1) {
23
- throw new Error(
24
- "WhatsApp Business API only supports 1:1 conversations"
25
- );
26
- }
27
- const user = input.users[0];
28
- if (!user) {
29
- throw new Error("WhatsApp space creation requires a user");
30
- }
31
- return { id: user.id };
32
- },
33
- },
34
-
35
- lifecycle: {
36
- createClient: async ({ config }): Promise<WhatsAppClient> => {
37
- return createClient({
38
- accessToken: config.accessToken,
39
- phoneNumberId: config.phoneNumberId,
40
- appSecret: config.appSecret ?? "",
41
- });
42
- },
43
-
44
- destroyClient: async ({ client }: { client: WhatsAppClient }) => {
45
- await client.close();
46
- },
47
- },
48
-
49
- events: {
50
- messages: ({ client }) => messages(client as WhatsAppClient),
51
- },
52
-
53
- actions: {
54
- send: async ({ space, content, client }) => {
55
- const wa = client as WhatsAppClient;
56
- for (const item of content) {
57
- await send(wa, space.id, item);
58
- }
59
- },
60
-
61
- reactToMessage: async ({ space, messageId, reaction, client }) => {
62
- await reactToMessage(
63
- client as WhatsAppClient,
64
- space.id,
65
- messageId,
66
- reaction
67
- );
68
- },
69
-
70
- replyToMessage: async ({ space, messageId, content, client }) => {
71
- const wa = client as WhatsAppClient;
72
- for (const item of content) {
73
- await replyToMessage(wa, space.id, messageId, item);
74
- }
75
- },
76
- },
77
- });
@@ -1,240 +0,0 @@
1
- import type {
2
- InboundMessage,
3
- WhatsAppClient,
4
- } from "@photon-ai/whatsapp-business";
5
- import type { Content } from "../../types/content";
6
- import { type ManagedStream, stream } from "../../utils/stream";
7
- import type { WhatsAppMessage } from "./types";
8
-
9
- const toMessage = async (
10
- client: WhatsAppClient,
11
- msg: InboundMessage
12
- ): Promise<WhatsAppMessage> => {
13
- const content = await mapContent(client, msg.content);
14
- return {
15
- id: msg.id,
16
- content,
17
- sender: { id: msg.from },
18
- space: { id: msg.from },
19
- timestamp: msg.timestamp,
20
- };
21
- };
22
-
23
- const mapContent = async (
24
- client: WhatsAppClient,
25
- content: InboundMessage["content"]
26
- ): Promise<Content[]> => {
27
- switch (content.type) {
28
- case "text":
29
- return [{ type: "plain_text", text: content.body }];
30
- case "image":
31
- case "video":
32
- case "audio":
33
- case "document":
34
- return [await downloadMedia(client, content.media)];
35
- case "sticker":
36
- return [
37
- {
38
- type: "custom",
39
- raw: { whatsapp_type: "sticker", ...content.sticker },
40
- },
41
- ];
42
- case "location":
43
- return [
44
- {
45
- type: "custom",
46
- raw: { whatsapp_type: "location", ...content.location },
47
- },
48
- ];
49
- case "contacts":
50
- return [
51
- {
52
- type: "custom",
53
- raw: { whatsapp_type: "contacts", contacts: content.contacts },
54
- },
55
- ];
56
- case "reaction":
57
- return [
58
- {
59
- type: "custom",
60
- raw: { whatsapp_type: "reaction", ...content.reaction },
61
- },
62
- ];
63
- case "interactive":
64
- return [
65
- {
66
- type: "custom",
67
- raw: { whatsapp_type: "interactive", ...content.interactive },
68
- },
69
- ];
70
- case "button":
71
- return [
72
- {
73
- type: "custom",
74
- raw: { whatsapp_type: "button", ...content.button },
75
- },
76
- ];
77
- case "order":
78
- return [
79
- { type: "custom", raw: { whatsapp_type: "order", ...content.order } },
80
- ];
81
- case "system":
82
- return [
83
- {
84
- type: "custom",
85
- raw: { whatsapp_type: "system", ...content.system },
86
- },
87
- ];
88
- default:
89
- return [{ type: "custom", raw: { whatsapp_type: "unknown" } }];
90
- }
91
- };
92
-
93
- const downloadMedia = async (
94
- client: WhatsAppClient,
95
- media: { id: string; mimeType: string; filename?: string }
96
- ): Promise<Content> => {
97
- try {
98
- const { url } = await client.media.getUrl(media.id);
99
- const response = await fetch(url);
100
- if (!response.ok) {
101
- throw new Error(`Media download failed: ${response.status}`);
102
- }
103
- const data = Buffer.from(await response.arrayBuffer());
104
- return {
105
- type: "attachment",
106
- data,
107
- mimeType: media.mimeType,
108
- name: media.filename ?? `media-${media.id}`,
109
- };
110
- } catch {
111
- return {
112
- type: "custom",
113
- raw: {
114
- whatsapp_type: "media_error",
115
- mediaId: media.id,
116
- mimeType: media.mimeType,
117
- },
118
- };
119
- }
120
- };
121
-
122
- const mimeToMediaType = (
123
- mimeType: string
124
- ): "image" | "video" | "audio" | "document" => {
125
- if (mimeType.startsWith("image/")) {
126
- return "image";
127
- }
128
- if (mimeType.startsWith("video/")) {
129
- return "video";
130
- }
131
- if (mimeType.startsWith("audio/")) {
132
- return "audio";
133
- }
134
- return "document";
135
- };
136
-
137
- export const messages = (
138
- client: WhatsAppClient
139
- ): ManagedStream<WhatsAppMessage> => {
140
- const eventStream = client.events
141
- .subscribe()
142
- .filter(
143
- (e): e is Extract<typeof e, { type: "message" }> => e.type === "message"
144
- );
145
-
146
- return stream<WhatsAppMessage>((emit, end) => {
147
- (async () => {
148
- try {
149
- for await (const event of eventStream) {
150
- const msg = await toMessage(client, event.message);
151
- emit(msg);
152
- }
153
- end();
154
- } catch (e) {
155
- end(e);
156
- }
157
- })();
158
- return () => eventStream.close();
159
- });
160
- };
161
-
162
- export const send = async (
163
- client: WhatsAppClient,
164
- spaceId: string,
165
- content: Content
166
- ): Promise<void> => {
167
- switch (content.type) {
168
- case "plain_text":
169
- await client.messages.send({ to: spaceId, text: content.text });
170
- break;
171
- case "attachment": {
172
- const { mediaId } = await client.media.upload({
173
- file: content.data,
174
- mimeType: content.mimeType,
175
- filename: content.name,
176
- });
177
- const mediaType = mimeToMediaType(content.mimeType);
178
- const mediaPayload =
179
- mediaType === "document"
180
- ? { id: mediaId, filename: content.name }
181
- : { id: mediaId };
182
- await client.messages.send({
183
- to: spaceId,
184
- [mediaType]: mediaPayload,
185
- } as Parameters<typeof client.messages.send>[0]);
186
- break;
187
- }
188
- default:
189
- break;
190
- }
191
- };
192
-
193
- export const reactToMessage = async (
194
- client: WhatsAppClient,
195
- spaceId: string,
196
- messageId: string,
197
- reaction: string
198
- ): Promise<void> => {
199
- await client.messages.send({
200
- to: spaceId,
201
- reaction: { messageId, emoji: reaction },
202
- });
203
- };
204
-
205
- export const replyToMessage = async (
206
- client: WhatsAppClient,
207
- spaceId: string,
208
- messageId: string,
209
- content: Content
210
- ): Promise<void> => {
211
- switch (content.type) {
212
- case "plain_text":
213
- await client.messages.send({
214
- to: spaceId,
215
- replyTo: messageId,
216
- text: content.text,
217
- });
218
- break;
219
- case "attachment": {
220
- const { mediaId } = await client.media.upload({
221
- file: content.data,
222
- mimeType: content.mimeType,
223
- filename: content.name,
224
- });
225
- const mediaType = mimeToMediaType(content.mimeType);
226
- const mediaPayload =
227
- mediaType === "document"
228
- ? { id: mediaId, filename: content.name }
229
- : { id: mediaId };
230
- await client.messages.send({
231
- to: spaceId,
232
- replyTo: messageId,
233
- [mediaType]: mediaPayload,
234
- } as Parameters<typeof client.messages.send>[0]);
235
- break;
236
- }
237
- default:
238
- break;
239
- }
240
- };
@@ -1,19 +0,0 @@
1
- import z from "zod";
2
- import type { SchemaMessage } from "../../platform/types";
3
-
4
- export const configSchema = z.object({
5
- accessToken: z.string().min(1),
6
- phoneNumberId: z.string().min(1),
7
- appSecret: z.string().optional(),
8
- });
9
-
10
- export const userSchema = z.object({});
11
-
12
- export const spaceSchema = z.object({
13
- id: z.string(),
14
- });
15
-
16
- export type WhatsAppMessage = SchemaMessage<
17
- typeof userSchema,
18
- typeof spaceSchema
19
- >;