spectrum-ts 0.1.1 → 0.2.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/dist/{chunk-UZ2CXPOD.js → chunk-LIRM7SBA.js} +3 -2
- package/dist/chunk-XOBTWTFC.js +67 -0
- package/dist/index.d.ts +47 -4
- package/dist/index.js +24 -14
- package/dist/providers/imessage/index.d.ts +1 -1
- package/dist/providers/imessage/index.js +12 -32
- package/dist/providers/terminal/index.d.ts +1 -1
- package/dist/providers/terminal/index.js +1 -1
- package/dist/providers/whatsapp-business/index.d.ts +41 -0
- package/dist/providers/whatsapp-business/index.js +280 -0
- package/dist/{types-DiKuSemh.d.ts → types-DQE0dQT4.d.ts} +3 -3
- package/package.json +7 -1
- package/src/index.ts +12 -0
- package/src/platform/define.ts +3 -4
- package/src/platform/types.ts +7 -3
- package/src/providers/imessage/auth.ts +8 -54
- package/src/providers/imessage/index.ts +8 -0
- package/src/providers/whatsapp-business/index.ts +77 -0
- package/src/providers/whatsapp-business/messages.ts +240 -0
- package/src/providers/whatsapp-business/types.ts +19 -0
- package/src/spectrum.ts +28 -15
- package/src/utils/cloud.ts +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectrum-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"types": "./dist/providers/terminal/index.d.ts",
|
|
25
25
|
"bun": "./src/providers/terminal/index.ts",
|
|
26
26
|
"default": "./dist/providers/terminal/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./providers/whatsapp-business": {
|
|
29
|
+
"types": "./dist/providers/whatsapp-business/index.d.ts",
|
|
30
|
+
"bun": "./src/providers/whatsapp-business/index.ts",
|
|
31
|
+
"default": "./dist/providers/whatsapp-business/index.js"
|
|
27
32
|
}
|
|
28
33
|
},
|
|
29
34
|
"scripts": {
|
|
@@ -32,6 +37,7 @@
|
|
|
32
37
|
},
|
|
33
38
|
"dependencies": {
|
|
34
39
|
"@photon-ai/advanced-imessage": "^0.1.0",
|
|
40
|
+
"@photon-ai/whatsapp-business": "^0.1.1",
|
|
35
41
|
"@photon-ai/imessage-kit": "^2.1.2",
|
|
36
42
|
"@repeaterjs/repeater": "^3.0.6",
|
|
37
43
|
"better-grpc": "^0.3.2",
|
package/src/index.ts
CHANGED
|
@@ -23,4 +23,16 @@ export {
|
|
|
23
23
|
export type { Message } from "./types/message";
|
|
24
24
|
export type { Space } from "./types/space";
|
|
25
25
|
export type { User } from "./types/user";
|
|
26
|
+
export type {
|
|
27
|
+
CloudPlatform,
|
|
28
|
+
DedicatedTokenData,
|
|
29
|
+
ImessageInfoData,
|
|
30
|
+
PlatformStatus,
|
|
31
|
+
PlatformsData,
|
|
32
|
+
SharedTokenData,
|
|
33
|
+
SubscriptionData,
|
|
34
|
+
SubscriptionStatus,
|
|
35
|
+
TokenData,
|
|
36
|
+
} from "./utils/cloud";
|
|
37
|
+
export { cloud, SpectrumCloudError } from "./utils/cloud";
|
|
26
38
|
export { type ManagedStream, mergeStreams, stream } from "./utils/stream";
|
package/src/platform/define.ts
CHANGED
|
@@ -289,14 +289,13 @@ export function definePlatform<
|
|
|
289
289
|
throw new Error("Invalid input to platform narrowing function");
|
|
290
290
|
}) as Platform<Def>;
|
|
291
291
|
|
|
292
|
-
narrower.config = (
|
|
293
|
-
|
|
294
|
-
) => {
|
|
292
|
+
narrower.config = (config?: z.input<_ConfigSchema>) => {
|
|
293
|
+
const resolvedConfig = config ?? {};
|
|
295
294
|
return {
|
|
296
295
|
__tag: "PlatformProviderConfig" as const,
|
|
297
296
|
__def: undefined as unknown as Def,
|
|
298
297
|
__name: name,
|
|
299
|
-
config,
|
|
298
|
+
config: resolvedConfig,
|
|
300
299
|
__definition: fullDef as AnyPlatformDef,
|
|
301
300
|
} satisfies PlatformProviderConfig<Def> as PlatformProviderConfig<Def>;
|
|
302
301
|
};
|
package/src/platform/types.ts
CHANGED
|
@@ -140,8 +140,8 @@ export interface PlatformDef<
|
|
|
140
140
|
lifecycle: {
|
|
141
141
|
createClient: (ctx: {
|
|
142
142
|
config: z.infer<_ConfigSchema>;
|
|
143
|
-
projectId: string;
|
|
144
|
-
projectSecret: string;
|
|
143
|
+
projectId: string | undefined;
|
|
144
|
+
projectSecret: string | undefined;
|
|
145
145
|
}) => Promise<_Client>;
|
|
146
146
|
destroyClient: (ctx: { client: _Client }) => Promise<void>;
|
|
147
147
|
};
|
|
@@ -423,7 +423,11 @@ export interface SpectrumLike<
|
|
|
423
423
|
// ---------------------------------------------------------------------------
|
|
424
424
|
|
|
425
425
|
export interface Platform<Def extends AnyPlatformDef> {
|
|
426
|
-
config(
|
|
426
|
+
config(
|
|
427
|
+
...args: Record<string, never> extends z.input<Def["config"]>
|
|
428
|
+
? [config?: z.input<Def["config"]>]
|
|
429
|
+
: [config: z.input<Def["config"]>]
|
|
430
|
+
): PlatformProviderConfig<Def>;
|
|
427
431
|
<Providers extends PlatformProviderConfig[]>(
|
|
428
432
|
spectrum: SpectrumLike<Providers>
|
|
429
433
|
): HasProvider<Providers, Def["name"]> extends true
|
|
@@ -2,73 +2,27 @@ import {
|
|
|
2
2
|
type AdvancedIMessage,
|
|
3
3
|
createClient,
|
|
4
4
|
} from "@photon-ai/advanced-imessage";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
cloud,
|
|
7
|
+
type DedicatedTokenData,
|
|
8
|
+
type SharedTokenData,
|
|
9
|
+
} from "../../utils/cloud";
|
|
6
10
|
|
|
7
11
|
const RENEWAL_RATIO = 0.8;
|
|
8
12
|
const EXPIRY_BUFFER_MS = 30_000;
|
|
9
13
|
const RETRY_DELAY_MS = 30_000;
|
|
10
14
|
|
|
11
|
-
interface SharedTokenData {
|
|
12
|
-
expiresIn: number;
|
|
13
|
-
token: string;
|
|
14
|
-
type: "shared";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface DedicatedTokenData {
|
|
18
|
-
auth: Record<string, string>;
|
|
19
|
-
expiresIn: number;
|
|
20
|
-
type: "dedicated";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type TokenData = SharedTokenData | DedicatedTokenData;
|
|
24
|
-
|
|
25
|
-
interface TokenResponse {
|
|
26
|
-
data: TokenData;
|
|
27
|
-
succeed: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
15
|
interface CloudAuth {
|
|
31
16
|
dispose: () => void;
|
|
32
17
|
}
|
|
33
18
|
|
|
34
19
|
const cloudAuthState = new WeakMap<AdvancedIMessage[], CloudAuth>();
|
|
35
20
|
|
|
36
|
-
async function fetchTokens(
|
|
37
|
-
projectId: string,
|
|
38
|
-
projectSecret: string
|
|
39
|
-
): Promise<TokenData> {
|
|
40
|
-
const url = `${SPECTRUM_CLOUD_URL}/${projectId}/imessage/tokens`;
|
|
41
|
-
const credentials = btoa(`${projectId}:${projectSecret}`);
|
|
42
|
-
|
|
43
|
-
const response = await fetch(url, {
|
|
44
|
-
method: "POST",
|
|
45
|
-
headers: {
|
|
46
|
-
Authorization: `Basic ${credentials}`,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
const body = await response.text().catch(() => "");
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Spectrum Cloud authentication failed (${response.status}): ${body || response.statusText}`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const json = (await response.json()) as TokenResponse;
|
|
58
|
-
if (!json.succeed) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
"Spectrum Cloud authentication failed: server returned succeed=false"
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return json.data;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
21
|
export async function createCloudClients(
|
|
68
22
|
projectId: string,
|
|
69
23
|
projectSecret: string
|
|
70
24
|
): Promise<AdvancedIMessage[]> {
|
|
71
|
-
let tokenData = await
|
|
25
|
+
let tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
72
26
|
let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1000;
|
|
73
27
|
let disposed = false;
|
|
74
28
|
let renewalTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -82,7 +36,7 @@ export async function createCloudClients(
|
|
|
82
36
|
|
|
83
37
|
renewalTimer = setTimeout(async () => {
|
|
84
38
|
try {
|
|
85
|
-
tokenData = await
|
|
39
|
+
tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
86
40
|
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1000;
|
|
87
41
|
scheduleRenewal();
|
|
88
42
|
} catch {
|
|
@@ -99,7 +53,7 @@ export async function createCloudClients(
|
|
|
99
53
|
if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
|
|
100
54
|
return;
|
|
101
55
|
}
|
|
102
|
-
tokenData = await
|
|
56
|
+
tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
103
57
|
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1000;
|
|
104
58
|
scheduleRenewal();
|
|
105
59
|
};
|
|
@@ -87,6 +87,14 @@ export const imessage = definePlatform("iMessage", {
|
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
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
|
+
|
|
90
98
|
return await createCloudClients(projectId, projectSecret);
|
|
91
99
|
},
|
|
92
100
|
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
>;
|
package/src/spectrum.ts
CHANGED
|
@@ -47,11 +47,18 @@ export type SpectrumInstance<
|
|
|
47
47
|
// Config validation
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
49
|
|
|
50
|
-
const spectrumConfigSchema = z.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
const spectrumConfigSchema = z.union([
|
|
51
|
+
z.object({
|
|
52
|
+
projectId: z.string().min(1),
|
|
53
|
+
projectSecret: z.string().min(1),
|
|
54
|
+
providers: z.array(z.custom<PlatformProviderConfig>()),
|
|
55
|
+
}),
|
|
56
|
+
z.object({
|
|
57
|
+
projectId: z.undefined().optional(),
|
|
58
|
+
projectSecret: z.undefined().optional(),
|
|
59
|
+
providers: z.array(z.custom<PlatformProviderConfig>()),
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
55
62
|
|
|
56
63
|
// ---------------------------------------------------------------------------
|
|
57
64
|
// Spectrum() factory
|
|
@@ -60,15 +67,21 @@ const spectrumConfigSchema = z.object({
|
|
|
60
67
|
export async function Spectrum<
|
|
61
68
|
const Providers extends PlatformProviderConfig[],
|
|
62
69
|
>(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
options:
|
|
71
|
+
| {
|
|
72
|
+
projectId: string;
|
|
73
|
+
projectSecret: string;
|
|
74
|
+
providers: [...Providers];
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
projectId?: never;
|
|
78
|
+
projectSecret?: never;
|
|
79
|
+
providers: [...Providers];
|
|
80
|
+
}
|
|
66
81
|
): Promise<SpectrumInstance<Providers>> {
|
|
67
|
-
spectrumConfigSchema.parse(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
providers: options.providers,
|
|
71
|
-
});
|
|
82
|
+
spectrumConfigSchema.parse(options);
|
|
83
|
+
|
|
84
|
+
const { projectId, projectSecret, providers } = options;
|
|
72
85
|
|
|
73
86
|
const platformStates = new Map<
|
|
74
87
|
string,
|
|
@@ -81,7 +94,7 @@ export async function Spectrum<
|
|
|
81
94
|
let stopped = false;
|
|
82
95
|
|
|
83
96
|
// Initialize all provider clients eagerly
|
|
84
|
-
for (const provider of
|
|
97
|
+
for (const provider of providers) {
|
|
85
98
|
const providerConfig = provider as PlatformProviderConfig;
|
|
86
99
|
const def = providerConfig.__definition;
|
|
87
100
|
const userConfig = def.config.parse(providerConfig.config);
|
|
@@ -344,7 +357,7 @@ export async function Spectrum<
|
|
|
344
357
|
);
|
|
345
358
|
|
|
346
359
|
const base = {
|
|
347
|
-
__providers:
|
|
360
|
+
__providers: providers,
|
|
348
361
|
__internal: { platforms: platformStates },
|
|
349
362
|
messages,
|
|
350
363
|
stop: stopOnce,
|