spectrum-ts 0.6.0 → 0.7.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-XZTTLPHE.js → chunk-6URE4AYH.js} +136 -24
- package/dist/{chunk-UZWRB3FZ.js → chunk-GX3JCGSD.js} +70 -1
- package/dist/index.d.ts +30 -4
- package/dist/index.js +5 -5
- package/dist/providers/imessage/index.d.ts +1 -1
- package/dist/providers/imessage/index.js +78 -70
- package/dist/providers/terminal/index.d.ts +1 -1
- package/dist/providers/terminal/index.js +3 -4
- package/dist/providers/whatsapp-business/index.d.ts +9 -8
- package/dist/providers/whatsapp-business/index.js +267 -22
- package/dist/{types-DZMHfgYQ.d.ts → types-B5tTx5hc.d.ts} +2 -2
- package/package.json +2 -2
- package/dist/chunk-HXM64ENV.js +0 -67
|
@@ -16,7 +16,78 @@ var resolveContents = (items) => Promise.all(
|
|
|
16
16
|
items.map((c) => typeof c === "string" ? text(c).build() : c.build())
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
+
// src/utils/errors.ts
|
|
20
|
+
var composeMessage = (opts) => {
|
|
21
|
+
const platform = opts.platform ?? "platform";
|
|
22
|
+
const subject = opts.kind === "content" ? `content type "${opts.contentType ?? "unknown"}"` : `action "${opts.action ?? "unknown"}"`;
|
|
23
|
+
const detail = opts.detail ? `: ${opts.detail}` : "";
|
|
24
|
+
return `${platform} does not support ${subject}${detail}`;
|
|
25
|
+
};
|
|
26
|
+
var UnsupportedError = class _UnsupportedError extends Error {
|
|
27
|
+
kind;
|
|
28
|
+
platform;
|
|
29
|
+
contentType;
|
|
30
|
+
action;
|
|
31
|
+
detail;
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
super(composeMessage(opts));
|
|
34
|
+
this.name = "UnsupportedError";
|
|
35
|
+
this.kind = opts.kind;
|
|
36
|
+
this.platform = opts.platform;
|
|
37
|
+
this.contentType = opts.contentType;
|
|
38
|
+
this.action = opts.action;
|
|
39
|
+
this.detail = opts.detail;
|
|
40
|
+
}
|
|
41
|
+
static content(contentType, platform, detail) {
|
|
42
|
+
return new _UnsupportedError({
|
|
43
|
+
kind: "content",
|
|
44
|
+
contentType,
|
|
45
|
+
platform,
|
|
46
|
+
detail
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
static action(action, platform, detail) {
|
|
50
|
+
return new _UnsupportedError({ kind: "action", action, platform, detail });
|
|
51
|
+
}
|
|
52
|
+
withPlatform(platform) {
|
|
53
|
+
if (this.platform) {
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
return new _UnsupportedError({
|
|
57
|
+
kind: this.kind,
|
|
58
|
+
platform,
|
|
59
|
+
contentType: this.contentType,
|
|
60
|
+
action: this.action,
|
|
61
|
+
detail: this.detail
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
19
66
|
// src/platform/build.ts
|
|
67
|
+
var ANSI_YELLOW = "\x1B[33m";
|
|
68
|
+
var ANSI_RESET = "\x1B[0m";
|
|
69
|
+
var supportsAnsiColor = () => {
|
|
70
|
+
if (typeof process === "undefined") {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (process.env.NO_COLOR) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const force = process.env.FORCE_COLOR;
|
|
77
|
+
if (force !== void 0) {
|
|
78
|
+
return force !== "" && force !== "0" && force !== "false";
|
|
79
|
+
}
|
|
80
|
+
return Boolean(process.stderr?.isTTY);
|
|
81
|
+
};
|
|
82
|
+
var warnUnsupported = (err, fallbackPlatform) => {
|
|
83
|
+
const platform = err.platform ?? fallbackPlatform;
|
|
84
|
+
const subject = err.kind === "content" ? `content type "${err.contentType ?? "unknown"}"` : `action "${err.action ?? "unknown"}"`;
|
|
85
|
+
const detail = err.detail ? `: ${err.detail}` : "";
|
|
86
|
+
const body = `[spectrum-ts] ${platform} does not support ${subject}${detail}; skipping.`;
|
|
87
|
+
console.warn(
|
|
88
|
+
supportsAnsiColor() ? `${ANSI_YELLOW}${body}${ANSI_RESET}` : body
|
|
89
|
+
);
|
|
90
|
+
};
|
|
20
91
|
function buildSpace(params) {
|
|
21
92
|
const { spaceRef, extras, typingCtx, definition, client, config } = params;
|
|
22
93
|
let space;
|
|
@@ -24,10 +95,19 @@ function buildSpace(params) {
|
|
|
24
95
|
const resolved = await resolveContents(content);
|
|
25
96
|
const results = [];
|
|
26
97
|
for (const item of resolved) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
98
|
+
let sendResult;
|
|
99
|
+
try {
|
|
100
|
+
sendResult = await definition.actions.send({
|
|
101
|
+
...typingCtx,
|
|
102
|
+
content: item
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof UnsupportedError) {
|
|
106
|
+
warnUnsupported(err, definition.name);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
31
111
|
if (!sendResult?.id) {
|
|
32
112
|
throw new Error(
|
|
33
113
|
`Platform "${definition.name}" send did not return a message id`
|
|
@@ -49,7 +129,10 @@ function buildSpace(params) {
|
|
|
49
129
|
})
|
|
50
130
|
);
|
|
51
131
|
}
|
|
52
|
-
|
|
132
|
+
if (content.length === 1) {
|
|
133
|
+
return results[0];
|
|
134
|
+
}
|
|
135
|
+
return results;
|
|
53
136
|
}
|
|
54
137
|
space = {
|
|
55
138
|
...extras,
|
|
@@ -80,6 +163,10 @@ function buildMessage(params) {
|
|
|
80
163
|
const { definition, client, config, spaceRef, space } = params;
|
|
81
164
|
const react = async (reaction) => {
|
|
82
165
|
if (!definition.actions.reactToMessage) {
|
|
166
|
+
warnUnsupported(
|
|
167
|
+
UnsupportedError.action("react", definition.name),
|
|
168
|
+
definition.name
|
|
169
|
+
);
|
|
83
170
|
return;
|
|
84
171
|
}
|
|
85
172
|
await definition.actions.reactToMessage({
|
|
@@ -92,20 +179,31 @@ function buildMessage(params) {
|
|
|
92
179
|
};
|
|
93
180
|
async function reply(...content) {
|
|
94
181
|
if (!definition.actions.replyToMessage) {
|
|
95
|
-
|
|
96
|
-
|
|
182
|
+
warnUnsupported(
|
|
183
|
+
UnsupportedError.action("reply", definition.name),
|
|
184
|
+
definition.name
|
|
97
185
|
);
|
|
186
|
+
return content.length === 1 ? void 0 : [];
|
|
98
187
|
}
|
|
99
188
|
const resolved = await resolveContents(content);
|
|
100
189
|
const results = [];
|
|
101
190
|
for (const item of resolved) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
191
|
+
let sendResult;
|
|
192
|
+
try {
|
|
193
|
+
sendResult = await definition.actions.replyToMessage({
|
|
194
|
+
space: spaceRef,
|
|
195
|
+
messageId: params.id,
|
|
196
|
+
content: item,
|
|
197
|
+
client,
|
|
198
|
+
config
|
|
199
|
+
});
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err instanceof UnsupportedError) {
|
|
202
|
+
warnUnsupported(err, definition.name);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
109
207
|
if (!sendResult?.id) {
|
|
110
208
|
throw new Error(
|
|
111
209
|
`Platform "${definition.name}" reply did not return a message id`
|
|
@@ -127,7 +225,10 @@ function buildMessage(params) {
|
|
|
127
225
|
})
|
|
128
226
|
);
|
|
129
227
|
}
|
|
130
|
-
|
|
228
|
+
if (content.length === 1) {
|
|
229
|
+
return results[0];
|
|
230
|
+
}
|
|
231
|
+
return results;
|
|
131
232
|
}
|
|
132
233
|
const senderWithPlatform = params.sender === void 0 ? void 0 : { ...params.sender, __platform: definition.name };
|
|
133
234
|
if (params.direction === "outbound") {
|
|
@@ -141,21 +242,31 @@ function buildMessage(params) {
|
|
|
141
242
|
reply,
|
|
142
243
|
edit: async (newContent) => {
|
|
143
244
|
if (!definition.actions.editMessage) {
|
|
144
|
-
|
|
145
|
-
|
|
245
|
+
warnUnsupported(
|
|
246
|
+
UnsupportedError.action("edit", definition.name),
|
|
247
|
+
definition.name
|
|
146
248
|
);
|
|
249
|
+
return;
|
|
147
250
|
}
|
|
148
251
|
const [resolved] = await resolveContents([newContent]);
|
|
149
252
|
if (!resolved) {
|
|
150
253
|
return;
|
|
151
254
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
255
|
+
try {
|
|
256
|
+
await definition.actions.editMessage({
|
|
257
|
+
space: spaceRef,
|
|
258
|
+
messageId: params.id,
|
|
259
|
+
content: resolved,
|
|
260
|
+
client,
|
|
261
|
+
config
|
|
262
|
+
});
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (err instanceof UnsupportedError) {
|
|
265
|
+
warnUnsupported(err, definition.name);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
159
270
|
},
|
|
160
271
|
sender: senderWithPlatform,
|
|
161
272
|
space,
|
|
@@ -335,6 +446,7 @@ export {
|
|
|
335
446
|
asText,
|
|
336
447
|
text,
|
|
337
448
|
resolveContents,
|
|
449
|
+
UnsupportedError,
|
|
338
450
|
buildSpace,
|
|
339
451
|
buildMessage,
|
|
340
452
|
definePlatform
|
|
@@ -607,6 +607,73 @@ function mergeStreams(streams) {
|
|
|
607
607
|
});
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
+
// src/utils/cloud.ts
|
|
611
|
+
var SPECTRUM_CLOUD_URL = `https://${process.env.SPECTRUM_CLOUD_URL ?? "spectrum.photon.codes"}`;
|
|
612
|
+
var SpectrumCloudError = class extends Error {
|
|
613
|
+
status;
|
|
614
|
+
code;
|
|
615
|
+
constructor(status, code, message) {
|
|
616
|
+
super(message);
|
|
617
|
+
this.name = "SpectrumCloudError";
|
|
618
|
+
this.status = status;
|
|
619
|
+
this.code = code;
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
var request = async (path, init) => {
|
|
623
|
+
const response = await fetch(`${SPECTRUM_CLOUD_URL}${path}`, init);
|
|
624
|
+
if (!response.ok) {
|
|
625
|
+
const body = await response.text().catch(() => "");
|
|
626
|
+
try {
|
|
627
|
+
const parsed = JSON.parse(body);
|
|
628
|
+
throw new SpectrumCloudError(
|
|
629
|
+
response.status,
|
|
630
|
+
parsed.code,
|
|
631
|
+
parsed.message
|
|
632
|
+
);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
if (error instanceof SpectrumCloudError) {
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
throw new SpectrumCloudError(
|
|
638
|
+
response.status,
|
|
639
|
+
"UNKNOWN",
|
|
640
|
+
body || response.statusText
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const json = await response.json();
|
|
645
|
+
if (!json.succeed) {
|
|
646
|
+
throw new SpectrumCloudError(
|
|
647
|
+
response.status,
|
|
648
|
+
"UNKNOWN",
|
|
649
|
+
"Server returned succeed=false"
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
return json.data;
|
|
653
|
+
};
|
|
654
|
+
var basicAuth = (projectId, projectSecret) => `Basic ${btoa(`${projectId}:${projectSecret}`)}`;
|
|
655
|
+
var cloud = {
|
|
656
|
+
getSubscription: (projectId) => request(`/projects/${projectId}/billing/subscription`),
|
|
657
|
+
issueImessageTokens: (projectId, projectSecret) => request(`/projects/${projectId}/imessage/tokens`, {
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers: { Authorization: basicAuth(projectId, projectSecret) }
|
|
660
|
+
}),
|
|
661
|
+
getImessageInfo: (projectId) => request(`/projects/${projectId}/imessage/`),
|
|
662
|
+
issueWhatsappBusinessTokens: (projectId, projectSecret) => request(`/projects/${projectId}/whatsapp-business/tokens`, {
|
|
663
|
+
method: "POST",
|
|
664
|
+
headers: { Authorization: basicAuth(projectId, projectSecret) }
|
|
665
|
+
}),
|
|
666
|
+
getPlatforms: (projectId) => request(`/projects/${projectId}/platforms/`),
|
|
667
|
+
togglePlatform: (projectId, projectSecret, platform, enabled) => request(`/projects/${projectId}/platforms/`, {
|
|
668
|
+
method: "PATCH",
|
|
669
|
+
headers: {
|
|
670
|
+
Authorization: basicAuth(projectId, projectSecret),
|
|
671
|
+
"Content-Type": "application/json"
|
|
672
|
+
},
|
|
673
|
+
body: JSON.stringify({ platform, enabled })
|
|
674
|
+
})
|
|
675
|
+
};
|
|
676
|
+
|
|
610
677
|
export {
|
|
611
678
|
readSchema,
|
|
612
679
|
streamSchema,
|
|
@@ -620,5 +687,7 @@ export {
|
|
|
620
687
|
asCustom,
|
|
621
688
|
custom,
|
|
622
689
|
stream,
|
|
623
|
-
mergeStreams
|
|
690
|
+
mergeStreams,
|
|
691
|
+
SpectrumCloudError,
|
|
692
|
+
cloud
|
|
624
693
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ContentBuilder, U as User, a as ContentInput, b as Content, P as ProviderMessage, c as PlatformDef, d as Platform, e as PlatformProviderConfig, S as SpectrumLike, f as CustomEventStreams, g as Space, I as InboundMessage, O as OutboundMessage } from './types-
|
|
2
|
-
export { A as AnyPlatformDef, E as EventProducer, M as Message, h as PlatformInstance, i as PlatformMessage, j as PlatformSpace, k as PlatformUser, l as SchemaMessage } from './types-
|
|
1
|
+
import { C as ContentBuilder, U as User, a as ContentInput, b as Content, P as ProviderMessage, c as PlatformDef, d as Platform, e as PlatformProviderConfig, S as SpectrumLike, f as CustomEventStreams, g as Space, I as InboundMessage, O as OutboundMessage } from './types-B5tTx5hc.js';
|
|
2
|
+
export { A as AnyPlatformDef, E as EventProducer, M as Message, h as PlatformInstance, i as PlatformMessage, j as PlatformSpace, k as PlatformUser, l as SchemaMessage } from './types-B5tTx5hc.js';
|
|
3
3
|
import vCard from 'vcf';
|
|
4
4
|
import z__default from 'zod';
|
|
5
5
|
export { M as ManagedStream, m as mergeStreams, s as stream } from './stream-DGy4geUK.js';
|
|
@@ -163,7 +163,7 @@ declare function definePlatform<_Name extends string, _ConfigSchema extends z__d
|
|
|
163
163
|
type SpectrumInstance<Providers extends PlatformProviderConfig[] = PlatformProviderConfig[]> = SpectrumLike<Providers> & CustomEventStreams<Providers> & {
|
|
164
164
|
readonly messages: AsyncIterable<[Space, InboundMessage]>;
|
|
165
165
|
stop(): Promise<void>;
|
|
166
|
-
send(space: Space, content: ContentInput): Promise<OutboundMessage>;
|
|
166
|
+
send(space: Space, content: ContentInput): Promise<OutboundMessage | undefined>;
|
|
167
167
|
send(space: Space, ...content: [ContentInput, ContentInput, ...ContentInput[]]): Promise<OutboundMessage[]>;
|
|
168
168
|
edit(message: OutboundMessage, newContent: ContentInput): Promise<void>;
|
|
169
169
|
responding<T>(space: Space, fn: () => T | Promise<T>): Promise<T>;
|
|
@@ -202,6 +202,11 @@ type PlatformsData = Record<CloudPlatform, PlatformStatus>;
|
|
|
202
202
|
interface ImessageInfoData {
|
|
203
203
|
type: "shared" | "dedicated";
|
|
204
204
|
}
|
|
205
|
+
interface WhatsappBusinessTokenData {
|
|
206
|
+
auth: Record<string, string>;
|
|
207
|
+
expiresIn: number;
|
|
208
|
+
numbers: Record<string, string | null>;
|
|
209
|
+
}
|
|
205
210
|
declare class SpectrumCloudError extends Error {
|
|
206
211
|
readonly status: number;
|
|
207
212
|
readonly code: string;
|
|
@@ -211,11 +216,32 @@ declare const cloud: {
|
|
|
211
216
|
getSubscription: (projectId: string) => Promise<SubscriptionData>;
|
|
212
217
|
issueImessageTokens: (projectId: string, projectSecret: string) => Promise<TokenData>;
|
|
213
218
|
getImessageInfo: (projectId: string) => Promise<ImessageInfoData>;
|
|
219
|
+
issueWhatsappBusinessTokens: (projectId: string, projectSecret: string) => Promise<WhatsappBusinessTokenData>;
|
|
214
220
|
getPlatforms: (projectId: string) => Promise<PlatformsData>;
|
|
215
221
|
togglePlatform: (projectId: string, projectSecret: string, platform: CloudPlatform, enabled: boolean) => Promise<PlatformsData>;
|
|
216
222
|
};
|
|
217
223
|
|
|
224
|
+
type UnsupportedKind = "content" | "action";
|
|
225
|
+
interface UnsupportedErrorOptions {
|
|
226
|
+
action?: string;
|
|
227
|
+
contentType?: string;
|
|
228
|
+
detail?: string;
|
|
229
|
+
kind: UnsupportedKind;
|
|
230
|
+
platform?: string;
|
|
231
|
+
}
|
|
232
|
+
declare class UnsupportedError extends Error {
|
|
233
|
+
readonly kind: UnsupportedKind;
|
|
234
|
+
readonly platform?: string;
|
|
235
|
+
readonly contentType?: string;
|
|
236
|
+
readonly action?: string;
|
|
237
|
+
readonly detail?: string;
|
|
238
|
+
constructor(opts: UnsupportedErrorOptions);
|
|
239
|
+
static content(contentType: string, platform?: string, detail?: string): UnsupportedError;
|
|
240
|
+
static action(action: string, platform?: string, detail?: string): UnsupportedError;
|
|
241
|
+
withPlatform(platform: string): UnsupportedError;
|
|
242
|
+
}
|
|
243
|
+
|
|
218
244
|
declare const fromVCard: (vcf: string) => ContactInput;
|
|
219
245
|
declare const toVCard: (contact: Contact) => Promise<string>;
|
|
220
246
|
|
|
221
|
-
export { type CloudPlatform, type Contact, type ContactAddress, type ContactDetails, type ContactEmail, type ContactInput, type ContactName, type ContactOrg, type ContactPhone, Content, ContentBuilder, ContentInput, type DedicatedTokenData, type ImessageInfoData, Platform, PlatformDef, PlatformProviderConfig, type PlatformStatus, type PlatformsData, type SharedTokenData, Space, Spectrum, SpectrumCloudError, type SpectrumInstance, type SubscriptionData, type SubscriptionStatus, type TokenData, User, type Voice, attachment, cloud, contact, custom, definePlatform, fromVCard, resolveContents, text, toVCard, voice };
|
|
247
|
+
export { type CloudPlatform, type Contact, type ContactAddress, type ContactDetails, type ContactEmail, type ContactInput, type ContactName, type ContactOrg, type ContactPhone, Content, ContentBuilder, ContentInput, type DedicatedTokenData, type ImessageInfoData, Platform, PlatformDef, PlatformProviderConfig, type PlatformStatus, type PlatformsData, type SharedTokenData, Space, Spectrum, SpectrumCloudError, type SpectrumInstance, type SubscriptionData, type SubscriptionStatus, type TokenData, UnsupportedError, type UnsupportedKind, User, type Voice, attachment, cloud, contact, custom, definePlatform, fromVCard, resolveContents, text, toVCard, voice };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SpectrumCloudError,
|
|
3
|
-
cloud
|
|
4
|
-
} from "./chunk-HXM64ENV.js";
|
|
5
|
-
import {
|
|
6
3
|
attachment,
|
|
7
4
|
bufferToStream,
|
|
5
|
+
cloud,
|
|
8
6
|
contact,
|
|
9
7
|
custom,
|
|
10
8
|
fromVCard,
|
|
@@ -13,14 +11,15 @@ import {
|
|
|
13
11
|
stream,
|
|
14
12
|
streamSchema,
|
|
15
13
|
toVCard
|
|
16
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GX3JCGSD.js";
|
|
17
15
|
import {
|
|
16
|
+
UnsupportedError,
|
|
18
17
|
buildMessage,
|
|
19
18
|
buildSpace,
|
|
20
19
|
definePlatform,
|
|
21
20
|
resolveContents,
|
|
22
21
|
text
|
|
23
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-6URE4AYH.js";
|
|
24
23
|
|
|
25
24
|
// src/content/voice.ts
|
|
26
25
|
import { createReadStream } from "fs";
|
|
@@ -367,6 +366,7 @@ async function Spectrum(options) {
|
|
|
367
366
|
export {
|
|
368
367
|
Spectrum,
|
|
369
368
|
SpectrumCloudError,
|
|
369
|
+
UnsupportedError,
|
|
370
370
|
attachment,
|
|
371
371
|
cloud,
|
|
372
372
|
contact,
|
|
@@ -3,7 +3,7 @@ import { AdvancedIMessage } from '@photon-ai/advanced-imessage';
|
|
|
3
3
|
import { IMessageSDK } from '@photon-ai/imessage-kit';
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import z__default from 'zod';
|
|
6
|
-
import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-
|
|
6
|
+
import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-B5tTx5hc.js';
|
|
7
7
|
import * as zod_v4_core from 'zod/v4/core';
|
|
8
8
|
import 'hotscript';
|
|
9
9
|
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
cloud
|
|
3
|
-
} from "../../chunk-HXM64ENV.js";
|
|
4
1
|
import {
|
|
5
2
|
asAttachment,
|
|
6
3
|
asContact,
|
|
7
4
|
asCustom,
|
|
5
|
+
cloud,
|
|
8
6
|
fromVCard,
|
|
9
7
|
mergeStreams,
|
|
10
8
|
stream,
|
|
11
9
|
toVCard
|
|
12
|
-
} from "../../chunk-
|
|
10
|
+
} from "../../chunk-GX3JCGSD.js";
|
|
13
11
|
import {
|
|
12
|
+
UnsupportedError,
|
|
14
13
|
asText,
|
|
15
14
|
definePlatform
|
|
16
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-6URE4AYH.js";
|
|
17
16
|
|
|
18
17
|
// src/providers/imessage/index.ts
|
|
19
18
|
import { createClient as createClient2, directChat } from "@photon-ai/advanced-imessage";
|
|
@@ -107,21 +106,14 @@ async function disposeCloudAuth(clients) {
|
|
|
107
106
|
|
|
108
107
|
// src/providers/imessage/local.ts
|
|
109
108
|
import { createReadStream } from "fs";
|
|
110
|
-
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
109
|
+
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
|
111
110
|
import { tmpdir } from "os";
|
|
112
111
|
import { basename, join } from "path";
|
|
113
112
|
import { Readable } from "stream";
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!result.message?.id) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
"iMessage local send did not return a message id \u2014 track upstream in @photon-ai/imessage-kit"
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
return { id: result.message.id, timestamp: result.sentAt };
|
|
124
|
-
};
|
|
113
|
+
var synthSendResult = () => ({
|
|
114
|
+
id: crypto.randomUUID(),
|
|
115
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
116
|
+
});
|
|
125
117
|
var DEFAULT_ATTACHMENT_NAME = "attachment";
|
|
126
118
|
var VCARD_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
127
119
|
"text/vcard",
|
|
@@ -137,17 +129,21 @@ var isVCardAttachment = (mimeType, fileName) => {
|
|
|
137
129
|
}
|
|
138
130
|
return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
|
|
139
131
|
};
|
|
140
|
-
var
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
132
|
+
var readLocalAttachment = async (att) => {
|
|
133
|
+
if (!att.localPath) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`iMessage attachment ${att.id} has no local file available on disk`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return readFile(att.localPath);
|
|
139
|
+
};
|
|
144
140
|
var toAttachmentContent = (att) => {
|
|
145
141
|
const { localPath } = att;
|
|
146
142
|
return asAttachment({
|
|
147
143
|
name: att.fileName ?? DEFAULT_ATTACHMENT_NAME,
|
|
148
144
|
mimeType: att.mimeType,
|
|
149
145
|
size: att.sizeBytes,
|
|
150
|
-
read: () =>
|
|
146
|
+
read: () => readLocalAttachment(att),
|
|
151
147
|
stream: localPath ? async () => Readable.toWeb(
|
|
152
148
|
createReadStream(localPath)
|
|
153
149
|
) : void 0
|
|
@@ -155,16 +151,23 @@ var toAttachmentContent = (att) => {
|
|
|
155
151
|
};
|
|
156
152
|
var toVCardContent = async (att) => {
|
|
157
153
|
try {
|
|
158
|
-
const buf = await
|
|
154
|
+
const buf = await readLocalAttachment(att);
|
|
159
155
|
return asContact(fromVCard(buf.toString("utf8")));
|
|
160
156
|
} catch {
|
|
161
157
|
return toAttachmentContent(att);
|
|
162
158
|
}
|
|
163
159
|
};
|
|
164
160
|
var toMessages = async (message) => {
|
|
161
|
+
const { chatId, chatKind } = message;
|
|
162
|
+
if (!chatId || chatKind === "unknown") {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
if (message.reaction !== null || message.kind !== "text" || message.retractedAt !== null) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
165
168
|
const base = {
|
|
166
169
|
sender: { id: message.participant ?? "" },
|
|
167
|
-
space:
|
|
170
|
+
space: { id: chatId, type: chatKind === "group" ? "group" : "dm" },
|
|
168
171
|
timestamp: message.createdAt
|
|
169
172
|
};
|
|
170
173
|
if (message.attachments.length > 0) {
|
|
@@ -186,19 +189,23 @@ var toMessages = async (message) => {
|
|
|
186
189
|
};
|
|
187
190
|
var messages = (client) => stream((emit, end) => {
|
|
188
191
|
let lastPromise = Promise.resolve();
|
|
189
|
-
client.startWatching({
|
|
190
|
-
|
|
191
|
-
if (message.isFromMe) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
192
|
+
const startPromise = client.startWatching({
|
|
193
|
+
onIncomingMessage: (message) => {
|
|
194
194
|
lastPromise = lastPromise.then(() => toMessages(message)).then((ms) => {
|
|
195
195
|
for (const m of ms) {
|
|
196
196
|
emit(m);
|
|
197
197
|
}
|
|
198
|
-
}).catch(
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
}).catch(end);
|
|
199
|
+
},
|
|
200
|
+
onError: end
|
|
201
|
+
}).catch(end);
|
|
202
|
+
return async () => {
|
|
203
|
+
await startPromise.catch(() => {
|
|
204
|
+
});
|
|
205
|
+
await client.stopWatching();
|
|
206
|
+
await lastPromise.catch(() => {
|
|
207
|
+
});
|
|
208
|
+
};
|
|
202
209
|
});
|
|
203
210
|
var vcardFileName = (content) => {
|
|
204
211
|
const base = content.name?.formatted ?? content.user?.id ?? "contact";
|
|
@@ -210,7 +217,7 @@ var sendTempFile = async (client, spaceId, name, data) => {
|
|
|
210
217
|
const tmp = join(dir, safeName);
|
|
211
218
|
await writeFile(tmp, data);
|
|
212
219
|
try {
|
|
213
|
-
|
|
220
|
+
await client.send({ to: spaceId, attachments: [tmp] });
|
|
214
221
|
} finally {
|
|
215
222
|
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
216
223
|
});
|
|
@@ -219,26 +226,23 @@ var sendTempFile = async (client, spaceId, name, data) => {
|
|
|
219
226
|
var send = async (client, spaceId, content) => {
|
|
220
227
|
switch (content.type) {
|
|
221
228
|
case "text":
|
|
222
|
-
|
|
229
|
+
await client.send({ to: spaceId, text: content.text });
|
|
230
|
+
return synthSendResult();
|
|
223
231
|
case "attachment":
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
);
|
|
232
|
+
await sendTempFile(client, spaceId, content.name, await content.read());
|
|
233
|
+
return synthSendResult();
|
|
227
234
|
case "contact": {
|
|
228
235
|
const vcf = await toVCard(content);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Buffer.from(vcf, "utf8")
|
|
235
|
-
)
|
|
236
|
+
await sendTempFile(
|
|
237
|
+
client,
|
|
238
|
+
spaceId,
|
|
239
|
+
vcardFileName(content),
|
|
240
|
+
Buffer.from(vcf, "utf8")
|
|
236
241
|
);
|
|
242
|
+
return synthSendResult();
|
|
237
243
|
}
|
|
238
244
|
default:
|
|
239
|
-
throw
|
|
240
|
-
`Unsupported iMessage local content type: ${content.type}`
|
|
241
|
-
);
|
|
245
|
+
throw UnsupportedError.content(content.type, "iMessage (local mode)");
|
|
242
246
|
}
|
|
243
247
|
};
|
|
244
248
|
|
|
@@ -251,7 +255,7 @@ import {
|
|
|
251
255
|
|
|
252
256
|
// src/utils/audio.ts
|
|
253
257
|
import { spawn } from "child_process";
|
|
254
|
-
import { mkdtemp as mkdtemp2, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
258
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
255
259
|
import { tmpdir as tmpdir2 } from "os";
|
|
256
260
|
import { join as join2 } from "path";
|
|
257
261
|
var M4A_BRANDS = /* @__PURE__ */ new Set([
|
|
@@ -358,7 +362,7 @@ var transcodeToM4a = async (buffer) => {
|
|
|
358
362
|
if (code !== 0) {
|
|
359
363
|
throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
|
|
360
364
|
}
|
|
361
|
-
const out = await
|
|
365
|
+
const out = await readFile2(outPath);
|
|
362
366
|
return { buffer: out, duration: parseDuration(stderr) };
|
|
363
367
|
} finally {
|
|
364
368
|
await rm2(dir, { recursive: true, force: true }).catch(() => {
|
|
@@ -373,7 +377,9 @@ var ensureM4a = async (buffer, mimeType) => {
|
|
|
373
377
|
};
|
|
374
378
|
|
|
375
379
|
// src/providers/imessage/remote.ts
|
|
376
|
-
var
|
|
380
|
+
var PLATFORM = "iMessage";
|
|
381
|
+
var unsupportedContent = (type) => UnsupportedError.content(type, PLATFORM);
|
|
382
|
+
var toSendResult = (receipt) => ({
|
|
377
383
|
id: receipt.guid,
|
|
378
384
|
timestamp: /* @__PURE__ */ new Date()
|
|
379
385
|
});
|
|
@@ -495,14 +501,14 @@ var send2 = async (clients, spaceId, content) => {
|
|
|
495
501
|
const chat = chatGuid(spaceId);
|
|
496
502
|
switch (content.type) {
|
|
497
503
|
case "text":
|
|
498
|
-
return
|
|
504
|
+
return toSendResult(await remote.messages.send(chat, content.text));
|
|
499
505
|
case "attachment": {
|
|
500
506
|
const attachment = await remote.attachments.upload({
|
|
501
507
|
data: await content.read(),
|
|
502
508
|
fileName: content.name,
|
|
503
509
|
mimeType: content.mimeType
|
|
504
510
|
});
|
|
505
|
-
return
|
|
511
|
+
return toSendResult(
|
|
506
512
|
await remote.messages.send(chat, "", {
|
|
507
513
|
attachment: attachment.guid
|
|
508
514
|
})
|
|
@@ -510,7 +516,7 @@ var send2 = async (clients, spaceId, content) => {
|
|
|
510
516
|
}
|
|
511
517
|
case "contact": {
|
|
512
518
|
const attachment = await sendContactAttachment(remote, content);
|
|
513
|
-
return
|
|
519
|
+
return toSendResult(await remote.messages.send(chat, "", { attachment }));
|
|
514
520
|
}
|
|
515
521
|
case "voice": {
|
|
516
522
|
const { buffer } = await ensureM4a(
|
|
@@ -522,7 +528,7 @@ var send2 = async (clients, spaceId, content) => {
|
|
|
522
528
|
fileName: content.name ?? "voice.m4a",
|
|
523
529
|
mimeType: "audio/x-m4a"
|
|
524
530
|
});
|
|
525
|
-
return
|
|
531
|
+
return toSendResult(
|
|
526
532
|
await remote.messages.send(chat, "", {
|
|
527
533
|
attachment: attachment.guid,
|
|
528
534
|
audioMessage: true
|
|
@@ -530,7 +536,7 @@ var send2 = async (clients, spaceId, content) => {
|
|
|
530
536
|
);
|
|
531
537
|
}
|
|
532
538
|
default:
|
|
533
|
-
throw
|
|
539
|
+
throw unsupportedContent(content.type);
|
|
534
540
|
}
|
|
535
541
|
};
|
|
536
542
|
var replyToMessage = async (clients, spaceId, msgId, content) => {
|
|
@@ -542,7 +548,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
|
|
|
542
548
|
const replyTo = messageGuid(msgId);
|
|
543
549
|
switch (content.type) {
|
|
544
550
|
case "text":
|
|
545
|
-
return
|
|
551
|
+
return toSendResult(
|
|
546
552
|
await remote.messages.send(chat, content.text, { replyTo })
|
|
547
553
|
);
|
|
548
554
|
case "attachment": {
|
|
@@ -551,7 +557,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
|
|
|
551
557
|
fileName: content.name,
|
|
552
558
|
mimeType: content.mimeType
|
|
553
559
|
});
|
|
554
|
-
return
|
|
560
|
+
return toSendResult(
|
|
555
561
|
await remote.messages.send(chat, "", {
|
|
556
562
|
attachment: attachment.guid,
|
|
557
563
|
replyTo
|
|
@@ -560,7 +566,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
|
|
|
560
566
|
}
|
|
561
567
|
case "contact": {
|
|
562
568
|
const attachment = await sendContactAttachment(remote, content);
|
|
563
|
-
return
|
|
569
|
+
return toSendResult(
|
|
564
570
|
await remote.messages.send(chat, "", { attachment, replyTo })
|
|
565
571
|
);
|
|
566
572
|
}
|
|
@@ -574,7 +580,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
|
|
|
574
580
|
fileName: content.name ?? "voice.m4a",
|
|
575
581
|
mimeType: "audio/x-m4a"
|
|
576
582
|
});
|
|
577
|
-
return
|
|
583
|
+
return toSendResult(
|
|
578
584
|
await remote.messages.send(chat, "", {
|
|
579
585
|
attachment: attachment.guid,
|
|
580
586
|
audioMessage: true,
|
|
@@ -583,12 +589,16 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
|
|
|
583
589
|
);
|
|
584
590
|
}
|
|
585
591
|
default:
|
|
586
|
-
throw
|
|
592
|
+
throw unsupportedContent(content.type);
|
|
587
593
|
}
|
|
588
594
|
};
|
|
589
595
|
var editMessage = async (clients, spaceId, msgId, content) => {
|
|
590
596
|
if (content.type !== "text") {
|
|
591
|
-
throw
|
|
597
|
+
throw UnsupportedError.content(
|
|
598
|
+
content.type,
|
|
599
|
+
PLATFORM,
|
|
600
|
+
"only text content can be edited"
|
|
601
|
+
);
|
|
592
602
|
}
|
|
593
603
|
const remote = clients[0];
|
|
594
604
|
if (!remote) {
|
|
@@ -652,8 +662,10 @@ var imessage = definePlatform("iMessage", {
|
|
|
652
662
|
schema: spaceSchema,
|
|
653
663
|
resolve: async ({ input, client }) => {
|
|
654
664
|
if (isLocal(client)) {
|
|
655
|
-
throw
|
|
656
|
-
"
|
|
665
|
+
throw UnsupportedError.action(
|
|
666
|
+
"createSpace",
|
|
667
|
+
"iMessage (local mode)",
|
|
668
|
+
"local mode only supports replying to existing messages"
|
|
657
669
|
);
|
|
658
670
|
}
|
|
659
671
|
if (input.users.length === 0) {
|
|
@@ -735,17 +747,13 @@ var imessage = definePlatform("iMessage", {
|
|
|
735
747
|
},
|
|
736
748
|
replyToMessage: async ({ space, messageId, content, client }) => {
|
|
737
749
|
if (isLocal(client)) {
|
|
738
|
-
throw
|
|
739
|
-
"iMessage local mode does not support replying to messages"
|
|
740
|
-
);
|
|
750
|
+
throw UnsupportedError.action("reply", "iMessage (local mode)");
|
|
741
751
|
}
|
|
742
752
|
return await replyToMessage(client, space.id, messageId, content);
|
|
743
753
|
},
|
|
744
754
|
editMessage: async ({ space, messageId, content, client }) => {
|
|
745
755
|
if (isLocal(client)) {
|
|
746
|
-
throw
|
|
747
|
-
"iMessage local mode does not support editing messages"
|
|
748
|
-
);
|
|
756
|
+
throw UnsupportedError.action("edit", "iMessage (local mode)");
|
|
749
757
|
}
|
|
750
758
|
await editMessage(client, space.id, messageId, content);
|
|
751
759
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-
|
|
1
|
+
import { d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-B5tTx5hc.js';
|
|
2
2
|
import * as node_readline from 'node:readline';
|
|
3
3
|
import z__default from 'zod';
|
|
4
4
|
import 'hotscript';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
UnsupportedError,
|
|
2
3
|
definePlatform
|
|
3
|
-
} from "../../chunk-
|
|
4
|
+
} from "../../chunk-6URE4AYH.js";
|
|
4
5
|
|
|
5
6
|
// src/providers/terminal/index.ts
|
|
6
7
|
import { createInterface } from "readline";
|
|
@@ -50,9 +51,7 @@ var terminal = definePlatform("terminal", {
|
|
|
50
51
|
actions: {
|
|
51
52
|
send: async ({ content }) => {
|
|
52
53
|
if (content.type !== "text") {
|
|
53
|
-
throw
|
|
54
|
-
`Terminal provider only supports text content, got "${content.type}"`
|
|
55
|
-
);
|
|
54
|
+
throw UnsupportedError.content(content.type, "terminal");
|
|
56
55
|
}
|
|
57
56
|
console.log(content.text);
|
|
58
57
|
return { id: crypto.randomUUID(), timestamp: /* @__PURE__ */ new Date() };
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { M as ManagedStream } from '../../stream-DGy4geUK.js';
|
|
2
|
+
import { WhatsAppClient } from '@photon-ai/whatsapp-business';
|
|
2
3
|
import * as z from 'zod';
|
|
3
4
|
import z__default from 'zod';
|
|
4
|
-
import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-
|
|
5
|
+
import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-B5tTx5hc.js';
|
|
5
6
|
import * as zod_v4_core from 'zod/v4/core';
|
|
6
|
-
import { WhatsAppClient } from '@photon-ai/whatsapp-business';
|
|
7
7
|
import 'hotscript';
|
|
8
8
|
|
|
9
|
+
type WhatsAppClients = WhatsAppClient[];
|
|
9
10
|
declare const userSchema: z__default.ZodObject<{}, z__default.core.$strip>;
|
|
10
11
|
declare const spaceSchema: z__default.ZodObject<{
|
|
11
12
|
id: z__default.ZodString;
|
|
12
13
|
}, z__default.core.$strip>;
|
|
13
14
|
type WhatsAppMessage = SchemaMessage<typeof userSchema, typeof spaceSchema>;
|
|
14
15
|
|
|
15
|
-
declare const whatsappBusiness: Platform<PlatformDef<"WhatsApp Business", z.ZodObject<{
|
|
16
|
+
declare const whatsappBusiness: Platform<PlatformDef<"WhatsApp Business", z.ZodUnion<readonly [z.ZodObject<{
|
|
16
17
|
accessToken: z.ZodString;
|
|
17
|
-
phoneNumberId: z.ZodString;
|
|
18
18
|
appSecret: z.ZodOptional<z.ZodString>;
|
|
19
|
-
|
|
19
|
+
phoneNumberId: z.ZodString;
|
|
20
|
+
}, zod_v4_core.$strip>, z.ZodObject<{}, zod_v4_core.$strict>]>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, z.ZodObject<{
|
|
20
21
|
id: z.ZodString;
|
|
21
|
-
}, zod_v4_core.$strip>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined,
|
|
22
|
+
}, zod_v4_core.$strip>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, WhatsAppClients, {
|
|
22
23
|
id: string;
|
|
23
24
|
}, {
|
|
24
25
|
id: string;
|
|
@@ -28,12 +29,12 @@ declare const whatsappBusiness: Platform<PlatformDef<"WhatsApp Business", z.ZodO
|
|
|
28
29
|
id: string;
|
|
29
30
|
}, Record<never, never>>, {
|
|
30
31
|
messages: ({ client }: {
|
|
31
|
-
client:
|
|
32
|
+
client: WhatsAppClients;
|
|
32
33
|
config: {
|
|
33
34
|
accessToken: string;
|
|
34
35
|
phoneNumberId: string;
|
|
35
36
|
appSecret?: string | undefined;
|
|
36
|
-
}
|
|
37
|
+
} | Record<string, never>;
|
|
37
38
|
}) => ManagedStream<WhatsAppMessage>;
|
|
38
39
|
}>> & Readonly<Record<never, never>>;
|
|
39
40
|
|
|
@@ -2,20 +2,242 @@ import {
|
|
|
2
2
|
asAttachment,
|
|
3
3
|
asContact,
|
|
4
4
|
asCustom,
|
|
5
|
+
cloud,
|
|
6
|
+
mergeStreams,
|
|
5
7
|
stream
|
|
6
|
-
} from "../../chunk-
|
|
8
|
+
} from "../../chunk-GX3JCGSD.js";
|
|
7
9
|
import {
|
|
10
|
+
UnsupportedError,
|
|
8
11
|
asText,
|
|
9
12
|
definePlatform
|
|
10
|
-
} from "../../chunk-
|
|
13
|
+
} from "../../chunk-6URE4AYH.js";
|
|
11
14
|
|
|
12
15
|
// src/providers/whatsapp-business/index.ts
|
|
16
|
+
import { createClient as createClient2 } from "@photon-ai/whatsapp-business";
|
|
17
|
+
|
|
18
|
+
// src/providers/whatsapp-business/auth.ts
|
|
13
19
|
import {
|
|
14
|
-
createClient
|
|
20
|
+
createClient,
|
|
21
|
+
TypedEventStream
|
|
15
22
|
} from "@photon-ai/whatsapp-business";
|
|
23
|
+
var RENEWAL_RATIO = 0.8;
|
|
24
|
+
var EXPIRY_BUFFER_MS = 3e4;
|
|
25
|
+
var RETRY_DELAY_MS = 3e4;
|
|
26
|
+
var RESUBSCRIBE_BACKOFF_MS = 500;
|
|
27
|
+
var cloudAuthState = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
async function createCloudClients(projectId, projectSecret) {
|
|
29
|
+
let tokenData = await cloud.issueWhatsappBusinessTokens(
|
|
30
|
+
projectId,
|
|
31
|
+
projectSecret
|
|
32
|
+
);
|
|
33
|
+
let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
34
|
+
let disposed = false;
|
|
35
|
+
let renewalTimer;
|
|
36
|
+
const lines = /* @__PURE__ */ new Map();
|
|
37
|
+
const buildRawClient = (phoneNumberId) => {
|
|
38
|
+
const accessToken = tokenData.auth[phoneNumberId];
|
|
39
|
+
if (!accessToken) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`WhatsApp Business line ${phoneNumberId} missing from token response`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return createClient({ accessToken, appSecret: "", phoneNumberId });
|
|
45
|
+
};
|
|
46
|
+
const refreshTokens = async () => {
|
|
47
|
+
tokenData = await cloud.issueWhatsappBusinessTokens(
|
|
48
|
+
projectId,
|
|
49
|
+
projectSecret
|
|
50
|
+
);
|
|
51
|
+
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
52
|
+
for (const [phoneNumberId, state] of lines) {
|
|
53
|
+
if (!tokenData.auth[phoneNumberId]) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const old = state.current;
|
|
57
|
+
state.current = buildRawClient(phoneNumberId);
|
|
58
|
+
for (const sub of state.subscriptions) {
|
|
59
|
+
sub.swap();
|
|
60
|
+
}
|
|
61
|
+
await old.close().catch(() => void 0);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const clearRenewalTimer = () => {
|
|
65
|
+
if (renewalTimer !== void 0) {
|
|
66
|
+
clearTimeout(renewalTimer);
|
|
67
|
+
renewalTimer = void 0;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const scheduleRenewal = () => {
|
|
71
|
+
if (disposed) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
clearRenewalTimer();
|
|
75
|
+
const ttlMs = tokenData.expiresIn * 1e3;
|
|
76
|
+
const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
|
|
77
|
+
renewalTimer = setTimeout(async () => {
|
|
78
|
+
try {
|
|
79
|
+
await refreshTokens();
|
|
80
|
+
scheduleRenewal();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn(
|
|
83
|
+
`[spectrum-ts] WhatsApp Business token refresh failed; retrying in ${RETRY_DELAY_MS}ms.`,
|
|
84
|
+
err
|
|
85
|
+
);
|
|
86
|
+
clearRenewalTimer();
|
|
87
|
+
renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
|
|
88
|
+
renewalTimer?.unref?.();
|
|
89
|
+
}
|
|
90
|
+
}, renewInMs);
|
|
91
|
+
renewalTimer?.unref?.();
|
|
92
|
+
};
|
|
93
|
+
const refreshIfNeeded = async () => {
|
|
94
|
+
if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await refreshTokens();
|
|
98
|
+
scheduleRenewal();
|
|
99
|
+
};
|
|
100
|
+
scheduleRenewal();
|
|
101
|
+
const clients = Object.keys(tokenData.auth).map(
|
|
102
|
+
(phoneNumberId) => {
|
|
103
|
+
const state = {
|
|
104
|
+
current: buildRawClient(phoneNumberId),
|
|
105
|
+
subscriptions: /* @__PURE__ */ new Set()
|
|
106
|
+
};
|
|
107
|
+
lines.set(phoneNumberId, state);
|
|
108
|
+
return buildClientProxy(state, refreshIfNeeded);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
cloudAuthState.set(clients, {
|
|
112
|
+
dispose: async () => {
|
|
113
|
+
disposed = true;
|
|
114
|
+
clearRenewalTimer();
|
|
115
|
+
for (const state of lines.values()) {
|
|
116
|
+
for (const sub of state.subscriptions) {
|
|
117
|
+
sub.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await Promise.allSettled(
|
|
121
|
+
Array.from(lines.values()).map((s) => s.current.close())
|
|
122
|
+
);
|
|
123
|
+
lines.clear();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return clients;
|
|
127
|
+
}
|
|
128
|
+
async function disposeCloudAuth(clients) {
|
|
129
|
+
const auth = cloudAuthState.get(clients);
|
|
130
|
+
if (!auth) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await auth.dispose();
|
|
134
|
+
cloudAuthState.delete(clients);
|
|
135
|
+
}
|
|
136
|
+
var buildClientProxy = (state, refresh) => {
|
|
137
|
+
const forwarder = (pick) => new Proxy({}, {
|
|
138
|
+
get: (_, prop) => {
|
|
139
|
+
return async (...args) => {
|
|
140
|
+
await refresh();
|
|
141
|
+
const target = pick(state.current);
|
|
142
|
+
const fn = target[prop];
|
|
143
|
+
return Reflect.apply(fn, pick(state.current), args);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const events = {
|
|
148
|
+
fetchMissed: async (opts) => {
|
|
149
|
+
await refresh();
|
|
150
|
+
return state.current.events.fetchMissed(opts);
|
|
151
|
+
},
|
|
152
|
+
subscribe: (options) => resubscribableStream(state, options)
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
events,
|
|
156
|
+
media: forwarder((c) => c.media),
|
|
157
|
+
messages: forwarder((c) => c.messages),
|
|
158
|
+
close: async () => {
|
|
159
|
+
for (const sub of state.subscriptions) {
|
|
160
|
+
sub.close();
|
|
161
|
+
}
|
|
162
|
+
await state.current.close();
|
|
163
|
+
},
|
|
164
|
+
[Symbol.asyncDispose]: async () => {
|
|
165
|
+
for (const sub of state.subscriptions) {
|
|
166
|
+
sub.close();
|
|
167
|
+
}
|
|
168
|
+
await state.current.close();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
var pumpOnce = async (ctx) => {
|
|
173
|
+
const sub = ctx.getCurrent().events.subscribe(ctx.options);
|
|
174
|
+
ctx.setActive(sub);
|
|
175
|
+
try {
|
|
176
|
+
for await (const event of sub) {
|
|
177
|
+
ctx.emit(event);
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
} finally {
|
|
183
|
+
ctx.setActive(void 0);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var resubscribableStream = (state, options) => {
|
|
187
|
+
let closed = false;
|
|
188
|
+
let active;
|
|
189
|
+
const source = stream((emit, end) => {
|
|
190
|
+
const ctx = {
|
|
191
|
+
emit,
|
|
192
|
+
getCurrent: () => state.current,
|
|
193
|
+
options,
|
|
194
|
+
setActive: (s) => {
|
|
195
|
+
active = s;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
(async () => {
|
|
199
|
+
while (!closed) {
|
|
200
|
+
await pumpOnce(ctx);
|
|
201
|
+
if (!closed) {
|
|
202
|
+
await new Promise((r) => setTimeout(r, RESUBSCRIBE_BACKOFF_MS));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
end();
|
|
206
|
+
})();
|
|
207
|
+
return () => {
|
|
208
|
+
closed = true;
|
|
209
|
+
active?.close().catch(() => void 0);
|
|
210
|
+
active = void 0;
|
|
211
|
+
state.subscriptions.delete(subscription);
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
const subscription = {
|
|
215
|
+
close: () => {
|
|
216
|
+
closed = true;
|
|
217
|
+
active?.close().catch(() => void 0);
|
|
218
|
+
},
|
|
219
|
+
swap: () => {
|
|
220
|
+
active?.close().catch(() => void 0);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
state.subscriptions.add(subscription);
|
|
224
|
+
return new TypedEventStream(source, async () => {
|
|
225
|
+
closed = true;
|
|
226
|
+
active?.close().catch(() => void 0);
|
|
227
|
+
state.subscriptions.delete(subscription);
|
|
228
|
+
await source.close();
|
|
229
|
+
});
|
|
230
|
+
};
|
|
16
231
|
|
|
17
232
|
// src/providers/whatsapp-business/messages.ts
|
|
18
233
|
import { extension as mimeExtension } from "mime-types";
|
|
234
|
+
var primary = (clients) => {
|
|
235
|
+
const client = clients[0];
|
|
236
|
+
if (!client) {
|
|
237
|
+
throw new Error("No WhatsApp Business client available");
|
|
238
|
+
}
|
|
239
|
+
return client;
|
|
240
|
+
};
|
|
19
241
|
var toSendResult = (result) => ({
|
|
20
242
|
id: result.messageId
|
|
21
243
|
});
|
|
@@ -291,7 +513,7 @@ var contactToWa = (contact) => {
|
|
|
291
513
|
};
|
|
292
514
|
return card;
|
|
293
515
|
};
|
|
294
|
-
var
|
|
516
|
+
var clientStream = (client) => {
|
|
295
517
|
const eventStream = client.events.subscribe().filter(
|
|
296
518
|
(e) => e.type === "message"
|
|
297
519
|
);
|
|
@@ -311,7 +533,9 @@ var messages = (client) => {
|
|
|
311
533
|
return () => eventStream.close();
|
|
312
534
|
});
|
|
313
535
|
};
|
|
314
|
-
var
|
|
536
|
+
var messages = (clients) => mergeStreams(clients.map(clientStream));
|
|
537
|
+
var send = async (clients, spaceId, content) => {
|
|
538
|
+
const client = primary(clients);
|
|
315
539
|
switch (content.type) {
|
|
316
540
|
case "text":
|
|
317
541
|
return toSendResult(
|
|
@@ -353,16 +577,17 @@ var send = async (client, spaceId, content) => {
|
|
|
353
577
|
);
|
|
354
578
|
}
|
|
355
579
|
default:
|
|
356
|
-
throw
|
|
580
|
+
throw UnsupportedError.content(content.type);
|
|
357
581
|
}
|
|
358
582
|
};
|
|
359
|
-
var reactToMessage = async (
|
|
360
|
-
await
|
|
583
|
+
var reactToMessage = async (clients, spaceId, messageId, reaction) => {
|
|
584
|
+
await primary(clients).messages.send({
|
|
361
585
|
to: spaceId,
|
|
362
586
|
reaction: { messageId, emoji: reaction }
|
|
363
587
|
});
|
|
364
588
|
};
|
|
365
|
-
var replyToMessage = async (
|
|
589
|
+
var replyToMessage = async (clients, spaceId, messageId, content) => {
|
|
590
|
+
const client = primary(clients);
|
|
366
591
|
switch (content.type) {
|
|
367
592
|
case "text":
|
|
368
593
|
return toSendResult(
|
|
@@ -411,17 +636,20 @@ var replyToMessage = async (client, spaceId, messageId, content) => {
|
|
|
411
636
|
);
|
|
412
637
|
}
|
|
413
638
|
default:
|
|
414
|
-
throw
|
|
639
|
+
throw UnsupportedError.content(content.type);
|
|
415
640
|
}
|
|
416
641
|
};
|
|
417
642
|
|
|
418
643
|
// src/providers/whatsapp-business/types.ts
|
|
419
644
|
import z from "zod";
|
|
420
|
-
var
|
|
645
|
+
var directConfig = z.object({
|
|
421
646
|
accessToken: z.string().min(1),
|
|
422
|
-
|
|
423
|
-
|
|
647
|
+
appSecret: z.string().optional(),
|
|
648
|
+
phoneNumberId: z.string().min(1)
|
|
424
649
|
});
|
|
650
|
+
var cloudConfig = z.object({}).strict();
|
|
651
|
+
var configSchema = z.union([directConfig, cloudConfig]);
|
|
652
|
+
var isCloudConfig = (config) => !("accessToken" in config);
|
|
425
653
|
var userSchema = z.object({});
|
|
426
654
|
var spaceSchema = z.object({
|
|
427
655
|
id: z.string()
|
|
@@ -440,8 +668,10 @@ var whatsappBusiness = definePlatform("WhatsApp Business", {
|
|
|
440
668
|
throw new Error("WhatsApp space creation requires at least one user");
|
|
441
669
|
}
|
|
442
670
|
if (input.users.length > 1) {
|
|
443
|
-
throw
|
|
444
|
-
"
|
|
671
|
+
throw UnsupportedError.action(
|
|
672
|
+
"createSpace",
|
|
673
|
+
"WhatsApp Business",
|
|
674
|
+
"only 1:1 conversations are supported"
|
|
445
675
|
);
|
|
446
676
|
}
|
|
447
677
|
const user = input.users[0];
|
|
@@ -452,15 +682,30 @@ var whatsappBusiness = definePlatform("WhatsApp Business", {
|
|
|
452
682
|
}
|
|
453
683
|
},
|
|
454
684
|
lifecycle: {
|
|
455
|
-
createClient: async ({
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
685
|
+
createClient: async ({
|
|
686
|
+
config,
|
|
687
|
+
projectId,
|
|
688
|
+
projectSecret
|
|
689
|
+
}) => {
|
|
690
|
+
if (!isCloudConfig(config)) {
|
|
691
|
+
return [
|
|
692
|
+
createClient2({
|
|
693
|
+
accessToken: config.accessToken,
|
|
694
|
+
appSecret: config.appSecret ?? "",
|
|
695
|
+
phoneNumberId: config.phoneNumberId
|
|
696
|
+
})
|
|
697
|
+
];
|
|
698
|
+
}
|
|
699
|
+
if (!(projectId && projectSecret)) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
"WhatsApp Business cloud mode requires projectId and projectSecret. Either pass credentials to Spectrum(), or provide direct credentials: whatsappBusiness.config({ accessToken, phoneNumberId })"
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
return await createCloudClients(projectId, projectSecret);
|
|
461
705
|
},
|
|
462
706
|
destroyClient: async ({ client }) => {
|
|
463
|
-
await client
|
|
707
|
+
await disposeCloudAuth(client);
|
|
708
|
+
await Promise.all(client.map((c) => c.close()));
|
|
464
709
|
}
|
|
465
710
|
},
|
|
466
711
|
events: {
|
|
@@ -95,7 +95,7 @@ interface Space<_Def = unknown> {
|
|
|
95
95
|
edit(message: OutboundMessage, newContent: ContentInput): Promise<void>;
|
|
96
96
|
readonly id: string;
|
|
97
97
|
responding<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
98
|
-
send(content: ContentInput): Promise<OutboundMessage>;
|
|
98
|
+
send(content: ContentInput): Promise<OutboundMessage | undefined>;
|
|
99
99
|
send(...content: [ContentInput, ContentInput, ...ContentInput[]]): Promise<OutboundMessage[]>;
|
|
100
100
|
startTyping(): Promise<void>;
|
|
101
101
|
stopTyping(): Promise<void>;
|
|
@@ -106,7 +106,7 @@ interface BaseMessage<TPlatform extends string = string, TSender extends User =
|
|
|
106
106
|
readonly id: string;
|
|
107
107
|
platform: TPlatform;
|
|
108
108
|
react(reaction: string): Promise<void>;
|
|
109
|
-
reply(content: ContentInput): Promise<OutboundMessage<TPlatform, TSender, TSpace
|
|
109
|
+
reply(content: ContentInput): Promise<OutboundMessage<TPlatform, TSender, TSpace> | undefined>;
|
|
110
110
|
reply(...content: [ContentInput, ContentInput, ...ContentInput[]]): Promise<OutboundMessage<TPlatform, TSender, TSpace>[]>;
|
|
111
111
|
space: TSpace;
|
|
112
112
|
timestamp: Date;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectrum-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@photon-ai/advanced-imessage": "^0.4.3",
|
|
23
|
-
"@photon-ai/imessage-kit": "^3.0.0
|
|
23
|
+
"@photon-ai/imessage-kit": "^3.0.0",
|
|
24
24
|
"@photon-ai/whatsapp-business": "^0.1.1",
|
|
25
25
|
"@repeaterjs/repeater": "^3.0.6",
|
|
26
26
|
"better-grpc": "^0.3.2",
|
package/dist/chunk-HXM64ENV.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// src/utils/cloud.ts
|
|
2
|
-
var SPECTRUM_CLOUD_URL = `https://${process.env.SPECTRUM_CLOUD_URL ?? "spectrum.photon.codes"}`;
|
|
3
|
-
var SpectrumCloudError = class extends Error {
|
|
4
|
-
status;
|
|
5
|
-
code;
|
|
6
|
-
constructor(status, code, message) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = "SpectrumCloudError";
|
|
9
|
-
this.status = status;
|
|
10
|
-
this.code = code;
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
var request = async (path, init) => {
|
|
14
|
-
const response = await fetch(`${SPECTRUM_CLOUD_URL}${path}`, init);
|
|
15
|
-
if (!response.ok) {
|
|
16
|
-
const body = await response.text().catch(() => "");
|
|
17
|
-
try {
|
|
18
|
-
const parsed = JSON.parse(body);
|
|
19
|
-
throw new SpectrumCloudError(
|
|
20
|
-
response.status,
|
|
21
|
-
parsed.code,
|
|
22
|
-
parsed.message
|
|
23
|
-
);
|
|
24
|
-
} catch (error) {
|
|
25
|
-
if (error instanceof SpectrumCloudError) {
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
throw new SpectrumCloudError(
|
|
29
|
-
response.status,
|
|
30
|
-
"UNKNOWN",
|
|
31
|
-
body || response.statusText
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const json = await response.json();
|
|
36
|
-
if (!json.succeed) {
|
|
37
|
-
throw new SpectrumCloudError(
|
|
38
|
-
response.status,
|
|
39
|
-
"UNKNOWN",
|
|
40
|
-
"Server returned succeed=false"
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
return json.data;
|
|
44
|
-
};
|
|
45
|
-
var basicAuth = (projectId, projectSecret) => `Basic ${btoa(`${projectId}:${projectSecret}`)}`;
|
|
46
|
-
var cloud = {
|
|
47
|
-
getSubscription: (projectId) => request(`/projects/${projectId}/billing/subscription`),
|
|
48
|
-
issueImessageTokens: (projectId, projectSecret) => request(`/projects/${projectId}/imessage/tokens`, {
|
|
49
|
-
method: "POST",
|
|
50
|
-
headers: { Authorization: basicAuth(projectId, projectSecret) }
|
|
51
|
-
}),
|
|
52
|
-
getImessageInfo: (projectId) => request(`/projects/${projectId}/imessage/`),
|
|
53
|
-
getPlatforms: (projectId) => request(`/projects/${projectId}/platforms/`),
|
|
54
|
-
togglePlatform: (projectId, projectSecret, platform, enabled) => request(`/projects/${projectId}/platforms/`, {
|
|
55
|
-
method: "PATCH",
|
|
56
|
-
headers: {
|
|
57
|
-
Authorization: basicAuth(projectId, projectSecret),
|
|
58
|
-
"Content-Type": "application/json"
|
|
59
|
-
},
|
|
60
|
-
body: JSON.stringify({ platform, enabled })
|
|
61
|
-
})
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export {
|
|
65
|
-
SpectrumCloudError,
|
|
66
|
-
cloud
|
|
67
|
-
};
|