spectrum-ts 1.4.0 → 1.7.1
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-2SB6VN7J.js +841 -0
- package/dist/chunk-4O7DPKLI.js +850 -0
- package/dist/chunk-H2QKKFQW.js +2254 -0
- package/dist/{chunk-LH4YEBG3.js → chunk-JWWIFSI7.js} +173 -185
- package/dist/{chunk-66GJ45ZZ.js → chunk-VO43HJ5B.js} +1 -1
- package/dist/{chunk-B4MHPWPZ.js → chunk-VVXMZYDH.js} +2 -2
- package/dist/{chunk-FF2R4EP3.js → chunk-WFIUWFE4.js} +4 -3
- package/dist/index.d.ts +549 -8
- package/dist/index.js +88 -92
- package/dist/providers/imessage/index.d.ts +33 -37
- package/dist/providers/imessage/index.js +8 -2097
- package/dist/providers/index.d.ts +12 -0
- package/dist/providers/index.js +18 -0
- package/dist/providers/terminal/index.d.ts +259 -31
- package/dist/providers/terminal/index.js +4 -838
- package/dist/providers/whatsapp-business/index.d.ts +2 -16
- package/dist/providers/whatsapp-business/index.js +4 -844
- package/dist/types-lUyzRurY.d.ts +1029 -0
- package/package.json +7 -3
- package/dist/types-BcCLW2VO.d.ts +0 -490
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asPollOption,
|
|
3
|
+
cloud,
|
|
4
|
+
mergeStreams,
|
|
5
|
+
stream
|
|
6
|
+
} from "./chunk-WFIUWFE4.js";
|
|
7
|
+
import {
|
|
8
|
+
UnsupportedError,
|
|
9
|
+
asAttachment,
|
|
10
|
+
asContact,
|
|
11
|
+
asCustom,
|
|
12
|
+
asReaction,
|
|
13
|
+
asText,
|
|
14
|
+
definePlatform
|
|
15
|
+
} from "./chunk-JWWIFSI7.js";
|
|
16
|
+
|
|
17
|
+
// src/providers/whatsapp-business/index.ts
|
|
18
|
+
import { createClient as createClient2 } from "@photon-ai/whatsapp-business";
|
|
19
|
+
|
|
20
|
+
// src/providers/whatsapp-business/auth.ts
|
|
21
|
+
import {
|
|
22
|
+
createClient,
|
|
23
|
+
TypedEventStream
|
|
24
|
+
} from "@photon-ai/whatsapp-business";
|
|
25
|
+
var RENEWAL_RATIO = 0.8;
|
|
26
|
+
var EXPIRY_BUFFER_MS = 3e4;
|
|
27
|
+
var RETRY_DELAY_MS = 3e4;
|
|
28
|
+
var RESUBSCRIBE_BACKOFF_MS = 500;
|
|
29
|
+
var cloudAuthState = /* @__PURE__ */ new WeakMap();
|
|
30
|
+
async function createCloudClients(projectId, projectSecret) {
|
|
31
|
+
let tokenData = await cloud.issueWhatsappBusinessTokens(
|
|
32
|
+
projectId,
|
|
33
|
+
projectSecret
|
|
34
|
+
);
|
|
35
|
+
let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
36
|
+
let disposed = false;
|
|
37
|
+
let renewalTimer;
|
|
38
|
+
const lines = /* @__PURE__ */ new Map();
|
|
39
|
+
const buildRawClient = (phoneNumberId) => {
|
|
40
|
+
const accessToken = tokenData.auth[phoneNumberId];
|
|
41
|
+
if (!accessToken) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`WhatsApp Business line ${phoneNumberId} missing from token response`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return createClient({ accessToken, appSecret: "", phoneNumberId });
|
|
47
|
+
};
|
|
48
|
+
const refreshTokens = async () => {
|
|
49
|
+
tokenData = await cloud.issueWhatsappBusinessTokens(
|
|
50
|
+
projectId,
|
|
51
|
+
projectSecret
|
|
52
|
+
);
|
|
53
|
+
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
54
|
+
for (const [phoneNumberId, state] of lines) {
|
|
55
|
+
if (!tokenData.auth[phoneNumberId]) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const old = state.current;
|
|
59
|
+
state.current = buildRawClient(phoneNumberId);
|
|
60
|
+
for (const sub of state.subscriptions) {
|
|
61
|
+
sub.swap();
|
|
62
|
+
}
|
|
63
|
+
await old.close().catch(() => void 0);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const clearRenewalTimer = () => {
|
|
67
|
+
if (renewalTimer !== void 0) {
|
|
68
|
+
clearTimeout(renewalTimer);
|
|
69
|
+
renewalTimer = void 0;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const scheduleRenewal = () => {
|
|
73
|
+
if (disposed) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
clearRenewalTimer();
|
|
77
|
+
const ttlMs = tokenData.expiresIn * 1e3;
|
|
78
|
+
const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
|
|
79
|
+
renewalTimer = setTimeout(async () => {
|
|
80
|
+
try {
|
|
81
|
+
await refreshTokens();
|
|
82
|
+
scheduleRenewal();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`[spectrum-ts] WhatsApp Business token refresh failed; retrying in ${RETRY_DELAY_MS}ms.`,
|
|
86
|
+
err
|
|
87
|
+
);
|
|
88
|
+
clearRenewalTimer();
|
|
89
|
+
renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
|
|
90
|
+
renewalTimer?.unref?.();
|
|
91
|
+
}
|
|
92
|
+
}, renewInMs);
|
|
93
|
+
renewalTimer?.unref?.();
|
|
94
|
+
};
|
|
95
|
+
const refreshIfNeeded = async () => {
|
|
96
|
+
if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await refreshTokens();
|
|
100
|
+
scheduleRenewal();
|
|
101
|
+
};
|
|
102
|
+
scheduleRenewal();
|
|
103
|
+
const clients = Object.keys(tokenData.auth).map(
|
|
104
|
+
(phoneNumberId) => {
|
|
105
|
+
const state = {
|
|
106
|
+
current: buildRawClient(phoneNumberId),
|
|
107
|
+
subscriptions: /* @__PURE__ */ new Set()
|
|
108
|
+
};
|
|
109
|
+
lines.set(phoneNumberId, state);
|
|
110
|
+
return buildClientProxy(state, refreshIfNeeded);
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
cloudAuthState.set(clients, {
|
|
114
|
+
dispose: async () => {
|
|
115
|
+
disposed = true;
|
|
116
|
+
clearRenewalTimer();
|
|
117
|
+
for (const state of lines.values()) {
|
|
118
|
+
for (const sub of state.subscriptions) {
|
|
119
|
+
sub.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await Promise.allSettled(
|
|
123
|
+
Array.from(lines.values()).map((s) => s.current.close())
|
|
124
|
+
);
|
|
125
|
+
lines.clear();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return clients;
|
|
129
|
+
}
|
|
130
|
+
async function disposeCloudAuth(clients) {
|
|
131
|
+
const auth = cloudAuthState.get(clients);
|
|
132
|
+
if (!auth) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await auth.dispose();
|
|
136
|
+
cloudAuthState.delete(clients);
|
|
137
|
+
}
|
|
138
|
+
var buildClientProxy = (state, refresh) => {
|
|
139
|
+
const forwarder = (pick) => new Proxy({}, {
|
|
140
|
+
get: (_, prop) => async (...args) => {
|
|
141
|
+
await refresh();
|
|
142
|
+
const target = pick(state.current);
|
|
143
|
+
const fn = target[prop];
|
|
144
|
+
return Reflect.apply(fn, pick(state.current), args);
|
|
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
|
+
await 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
|
+
const pump = (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 async () => {
|
|
208
|
+
closed = true;
|
|
209
|
+
active?.close().catch(() => void 0);
|
|
210
|
+
active = void 0;
|
|
211
|
+
state.subscriptions.delete(subscription);
|
|
212
|
+
await pump;
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
const subscription = {
|
|
216
|
+
close: () => {
|
|
217
|
+
closed = true;
|
|
218
|
+
active?.close().catch(() => void 0);
|
|
219
|
+
},
|
|
220
|
+
swap: () => {
|
|
221
|
+
active?.close().catch(() => void 0);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
state.subscriptions.add(subscription);
|
|
225
|
+
return new TypedEventStream(source, async () => {
|
|
226
|
+
closed = true;
|
|
227
|
+
active?.close().catch(() => void 0);
|
|
228
|
+
state.subscriptions.delete(subscription);
|
|
229
|
+
await source.close();
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/providers/whatsapp-business/messages.ts
|
|
234
|
+
import { extension as mimeExtension } from "mime-types";
|
|
235
|
+
|
|
236
|
+
// src/providers/whatsapp-business/poll.ts
|
|
237
|
+
import {
|
|
238
|
+
button,
|
|
239
|
+
buttons,
|
|
240
|
+
list
|
|
241
|
+
} from "@photon-ai/whatsapp-business";
|
|
242
|
+
var MAX_BUTTON_OPTIONS = 3;
|
|
243
|
+
var LIST_BUTTON_TEXT = "View options";
|
|
244
|
+
var LIST_SECTION_TITLE = "Options";
|
|
245
|
+
var pollOptionId = (index) => `opt_${index}`;
|
|
246
|
+
var pollToInteractive = (content) => {
|
|
247
|
+
if (content.options.length <= MAX_BUTTON_OPTIONS) {
|
|
248
|
+
return buttons(
|
|
249
|
+
content.title,
|
|
250
|
+
...content.options.map((o, i) => button(pollOptionId(i), o.title))
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return list(content.title, LIST_BUTTON_TEXT).section(
|
|
254
|
+
LIST_SECTION_TITLE,
|
|
255
|
+
content.options.map((o, i) => ({ id: pollOptionId(i), title: o.title }))
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/providers/whatsapp-business/messages.ts
|
|
260
|
+
var primary = (clients) => {
|
|
261
|
+
const client = clients[0];
|
|
262
|
+
if (!client) {
|
|
263
|
+
throw new Error("No WhatsApp Business client available");
|
|
264
|
+
}
|
|
265
|
+
return client;
|
|
266
|
+
};
|
|
267
|
+
var toRecord = (result, spaceId, content) => ({
|
|
268
|
+
id: result.messageId,
|
|
269
|
+
content,
|
|
270
|
+
space: { id: spaceId },
|
|
271
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
272
|
+
});
|
|
273
|
+
var MAX_POLL_CACHE_SIZE = 1e3;
|
|
274
|
+
var OPTION_ID_PREFIX = "opt_";
|
|
275
|
+
var pollCaches = /* @__PURE__ */ new WeakMap();
|
|
276
|
+
var getPollCache = (client) => {
|
|
277
|
+
let cache = pollCaches.get(client);
|
|
278
|
+
if (!cache) {
|
|
279
|
+
cache = /* @__PURE__ */ new Map();
|
|
280
|
+
pollCaches.set(client, cache);
|
|
281
|
+
}
|
|
282
|
+
return cache;
|
|
283
|
+
};
|
|
284
|
+
var cachePoll = (client, messageId, poll) => {
|
|
285
|
+
const cache = getPollCache(client);
|
|
286
|
+
if (cache.has(messageId)) {
|
|
287
|
+
cache.delete(messageId);
|
|
288
|
+
}
|
|
289
|
+
cache.set(messageId, poll);
|
|
290
|
+
if (cache.size > MAX_POLL_CACHE_SIZE) {
|
|
291
|
+
const first = cache.keys().next().value;
|
|
292
|
+
if (first !== void 0) {
|
|
293
|
+
cache.delete(first);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var optionIndexFromId = (id) => {
|
|
298
|
+
if (!id.startsWith(OPTION_ID_PREFIX)) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const index = Number(id.slice(OPTION_ID_PREFIX.length));
|
|
302
|
+
if (!Number.isInteger(index) || index < 0 || pollOptionId(index) !== id) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
return index;
|
|
306
|
+
};
|
|
307
|
+
var mapWaPhoneType = (type) => {
|
|
308
|
+
if (!type) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const upper = type.toUpperCase();
|
|
312
|
+
if (upper === "CELL" || upper === "MOBILE" || upper === "IPHONE") {
|
|
313
|
+
return "mobile";
|
|
314
|
+
}
|
|
315
|
+
if (upper === "HOME") {
|
|
316
|
+
return "home";
|
|
317
|
+
}
|
|
318
|
+
if (upper === "WORK" || upper === "BUSINESS") {
|
|
319
|
+
return "work";
|
|
320
|
+
}
|
|
321
|
+
return "other";
|
|
322
|
+
};
|
|
323
|
+
var mapWaSimpleType = (type) => {
|
|
324
|
+
if (!type) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const upper = type.toUpperCase();
|
|
328
|
+
if (upper === "HOME") {
|
|
329
|
+
return "home";
|
|
330
|
+
}
|
|
331
|
+
if (upper === "WORK" || upper === "BUSINESS") {
|
|
332
|
+
return "work";
|
|
333
|
+
}
|
|
334
|
+
return "other";
|
|
335
|
+
};
|
|
336
|
+
var waNameToSpectrum = (name) => {
|
|
337
|
+
const result = { formatted: name.formattedName };
|
|
338
|
+
if (name.firstName) {
|
|
339
|
+
result.first = name.firstName;
|
|
340
|
+
}
|
|
341
|
+
if (name.lastName) {
|
|
342
|
+
result.last = name.lastName;
|
|
343
|
+
}
|
|
344
|
+
if (name.middleName) {
|
|
345
|
+
result.middle = name.middleName;
|
|
346
|
+
}
|
|
347
|
+
if (name.prefix) {
|
|
348
|
+
result.prefix = name.prefix;
|
|
349
|
+
}
|
|
350
|
+
if (name.suffix) {
|
|
351
|
+
result.suffix = name.suffix;
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
};
|
|
355
|
+
var waPhoneToSpectrum = (phone) => {
|
|
356
|
+
const entry = { value: phone.phone };
|
|
357
|
+
const type = mapWaPhoneType(phone.type);
|
|
358
|
+
if (type) {
|
|
359
|
+
entry.type = type;
|
|
360
|
+
}
|
|
361
|
+
return entry;
|
|
362
|
+
};
|
|
363
|
+
var waEmailToSpectrum = (email) => {
|
|
364
|
+
const entry = { value: email.email };
|
|
365
|
+
const type = mapWaSimpleType(email.type);
|
|
366
|
+
if (type) {
|
|
367
|
+
entry.type = type;
|
|
368
|
+
}
|
|
369
|
+
return entry;
|
|
370
|
+
};
|
|
371
|
+
var waAddressToSpectrum = (address) => {
|
|
372
|
+
const entry = {};
|
|
373
|
+
if (address.street) {
|
|
374
|
+
entry.street = address.street;
|
|
375
|
+
}
|
|
376
|
+
if (address.city) {
|
|
377
|
+
entry.city = address.city;
|
|
378
|
+
}
|
|
379
|
+
if (address.state) {
|
|
380
|
+
entry.region = address.state;
|
|
381
|
+
}
|
|
382
|
+
if (address.zip) {
|
|
383
|
+
entry.postalCode = address.zip;
|
|
384
|
+
}
|
|
385
|
+
if (address.country) {
|
|
386
|
+
entry.country = address.country;
|
|
387
|
+
}
|
|
388
|
+
const type = mapWaSimpleType(address.type);
|
|
389
|
+
if (type) {
|
|
390
|
+
entry.type = type;
|
|
391
|
+
}
|
|
392
|
+
return entry;
|
|
393
|
+
};
|
|
394
|
+
var waOrgToSpectrum = (org) => {
|
|
395
|
+
const entry = {};
|
|
396
|
+
if (org.company) {
|
|
397
|
+
entry.name = org.company;
|
|
398
|
+
}
|
|
399
|
+
if (org.title) {
|
|
400
|
+
entry.title = org.title;
|
|
401
|
+
}
|
|
402
|
+
if (org.department) {
|
|
403
|
+
entry.department = org.department;
|
|
404
|
+
}
|
|
405
|
+
return entry;
|
|
406
|
+
};
|
|
407
|
+
var waContactToSpectrum = (card) => {
|
|
408
|
+
const input = { raw: card };
|
|
409
|
+
input.name = waNameToSpectrum(card.name);
|
|
410
|
+
if (card.phones.length > 0) {
|
|
411
|
+
input.phones = card.phones.map(waPhoneToSpectrum);
|
|
412
|
+
}
|
|
413
|
+
if (card.emails.length > 0) {
|
|
414
|
+
input.emails = card.emails.map(waEmailToSpectrum);
|
|
415
|
+
}
|
|
416
|
+
if (card.addresses.length > 0) {
|
|
417
|
+
input.addresses = card.addresses.map(waAddressToSpectrum);
|
|
418
|
+
}
|
|
419
|
+
if (card.org) {
|
|
420
|
+
input.org = waOrgToSpectrum(card.org);
|
|
421
|
+
}
|
|
422
|
+
if (card.urls.length > 0) {
|
|
423
|
+
input.urls = card.urls.map((u) => u.url);
|
|
424
|
+
}
|
|
425
|
+
if (card.birthday) {
|
|
426
|
+
input.birthday = card.birthday;
|
|
427
|
+
}
|
|
428
|
+
return asContact(input);
|
|
429
|
+
};
|
|
430
|
+
var toMessages = (client, msg) => {
|
|
431
|
+
const base = {
|
|
432
|
+
sender: { id: msg.from },
|
|
433
|
+
space: { id: msg.from },
|
|
434
|
+
timestamp: msg.timestamp
|
|
435
|
+
};
|
|
436
|
+
if (msg.content.type === "contacts") {
|
|
437
|
+
const multi = msg.content.contacts.length > 1;
|
|
438
|
+
return msg.content.contacts.map((card, index) => ({
|
|
439
|
+
...base,
|
|
440
|
+
id: multi ? `${msg.id}:${index}` : msg.id,
|
|
441
|
+
content: waContactToSpectrum(card)
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
return [
|
|
445
|
+
{
|
|
446
|
+
...base,
|
|
447
|
+
id: msg.id,
|
|
448
|
+
content: mapContent(client, msg)
|
|
449
|
+
}
|
|
450
|
+
];
|
|
451
|
+
};
|
|
452
|
+
var mapContent = (client, msg) => {
|
|
453
|
+
const { content } = msg;
|
|
454
|
+
switch (content.type) {
|
|
455
|
+
case "text":
|
|
456
|
+
return asText(content.body);
|
|
457
|
+
case "image":
|
|
458
|
+
case "video":
|
|
459
|
+
case "audio":
|
|
460
|
+
case "document":
|
|
461
|
+
return lazyMedia(client, content.media);
|
|
462
|
+
case "sticker":
|
|
463
|
+
return asCustom({ whatsapp_type: "sticker", ...content.sticker });
|
|
464
|
+
case "location":
|
|
465
|
+
return asCustom({ whatsapp_type: "location", ...content.location });
|
|
466
|
+
case "reaction": {
|
|
467
|
+
const stubTarget = {
|
|
468
|
+
id: content.reaction.messageId,
|
|
469
|
+
content: asCustom({ whatsapp_type: "reaction-target", stub: true })
|
|
470
|
+
};
|
|
471
|
+
return asReaction({
|
|
472
|
+
emoji: content.reaction.emoji,
|
|
473
|
+
target: stubTarget
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
case "interactive": {
|
|
477
|
+
const inter = content.interactive;
|
|
478
|
+
if (inter.type === "button_reply" || inter.type === "list_reply") {
|
|
479
|
+
const poll = msg.context?.id === void 0 ? void 0 : getPollCache(client).get(msg.context.id);
|
|
480
|
+
const optionIndex = optionIndexFromId(inter.reply.id);
|
|
481
|
+
const option = optionIndex === void 0 ? void 0 : poll?.options[optionIndex];
|
|
482
|
+
if (poll && option) {
|
|
483
|
+
return asPollOption({ poll, option, selected: true });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return asCustom({ whatsapp_type: "interactive", ...inter });
|
|
487
|
+
}
|
|
488
|
+
case "button":
|
|
489
|
+
return asCustom({ whatsapp_type: "button", ...content.button });
|
|
490
|
+
case "order":
|
|
491
|
+
return asCustom({ whatsapp_type: "order", ...content.order });
|
|
492
|
+
case "system":
|
|
493
|
+
return asCustom({ whatsapp_type: "system", ...content.system });
|
|
494
|
+
default:
|
|
495
|
+
return asCustom({ whatsapp_type: "unknown" });
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
var fetchMedia = async (client, mediaId) => {
|
|
499
|
+
const { url } = await client.media.getUrl(mediaId);
|
|
500
|
+
const response = await fetch(url);
|
|
501
|
+
if (!response.ok) {
|
|
502
|
+
throw new Error(`Media download failed: ${response.status}`);
|
|
503
|
+
}
|
|
504
|
+
return response;
|
|
505
|
+
};
|
|
506
|
+
var lazyMedia = (client, media) => asAttachment({
|
|
507
|
+
name: media.filename ?? `media-${media.id}`,
|
|
508
|
+
mimeType: media.mimeType,
|
|
509
|
+
read: async () => Buffer.from(await (await fetchMedia(client, media.id)).arrayBuffer()),
|
|
510
|
+
stream: async () => {
|
|
511
|
+
const response = await fetchMedia(client, media.id);
|
|
512
|
+
if (!response.body) {
|
|
513
|
+
throw new Error("Media response missing body");
|
|
514
|
+
}
|
|
515
|
+
return response.body;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
var mimeToMediaType = (mimeType) => {
|
|
519
|
+
if (mimeType.startsWith("image/")) {
|
|
520
|
+
return "image";
|
|
521
|
+
}
|
|
522
|
+
if (mimeType.startsWith("video/")) {
|
|
523
|
+
return "video";
|
|
524
|
+
}
|
|
525
|
+
if (mimeType.startsWith("audio/")) {
|
|
526
|
+
return "audio";
|
|
527
|
+
}
|
|
528
|
+
return "document";
|
|
529
|
+
};
|
|
530
|
+
var voiceFilename = (content) => {
|
|
531
|
+
if (content.name) {
|
|
532
|
+
return content.name;
|
|
533
|
+
}
|
|
534
|
+
const ext = mimeExtension(content.mimeType);
|
|
535
|
+
return ext ? `voice.${ext}` : "voice";
|
|
536
|
+
};
|
|
537
|
+
var spectrumPhoneTypeToWa = (type) => {
|
|
538
|
+
if (type === "mobile") {
|
|
539
|
+
return "CELL";
|
|
540
|
+
}
|
|
541
|
+
if (type === "home" || type === "work" || type === "other") {
|
|
542
|
+
return type.toUpperCase();
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
};
|
|
546
|
+
var spectrumSimpleTypeToWa = (type) => type ? type.toUpperCase() : void 0;
|
|
547
|
+
var spectrumNameToWa = (name) => ({
|
|
548
|
+
formattedName: name?.formatted ?? ([name?.first, name?.middle, name?.last].filter((p) => Boolean(p)).join(" ") || "Unknown"),
|
|
549
|
+
firstName: name?.first,
|
|
550
|
+
lastName: name?.last,
|
|
551
|
+
middleName: name?.middle,
|
|
552
|
+
prefix: name?.prefix,
|
|
553
|
+
suffix: name?.suffix
|
|
554
|
+
});
|
|
555
|
+
var isWhatsAppContactCard = (value) => {
|
|
556
|
+
if (!value || typeof value !== "object") {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
const raw = value;
|
|
560
|
+
const name = raw.name;
|
|
561
|
+
if (!name || typeof name !== "object" || typeof name.formattedName !== "string") {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
return Array.isArray(raw.phones) && Array.isArray(raw.emails) && Array.isArray(raw.addresses) && Array.isArray(raw.urls);
|
|
565
|
+
};
|
|
566
|
+
var contactToWa = (contact) => {
|
|
567
|
+
if (isWhatsAppContactCard(contact.raw)) {
|
|
568
|
+
return contact.raw;
|
|
569
|
+
}
|
|
570
|
+
const card = {
|
|
571
|
+
name: spectrumNameToWa(contact.name),
|
|
572
|
+
phones: (contact.phones ?? []).map((p) => ({
|
|
573
|
+
phone: p.value,
|
|
574
|
+
type: spectrumPhoneTypeToWa(p.type)
|
|
575
|
+
})),
|
|
576
|
+
emails: (contact.emails ?? []).map((e) => ({
|
|
577
|
+
email: e.value,
|
|
578
|
+
type: spectrumSimpleTypeToWa(e.type)
|
|
579
|
+
})),
|
|
580
|
+
addresses: (contact.addresses ?? []).map((a) => ({
|
|
581
|
+
street: a.street,
|
|
582
|
+
city: a.city,
|
|
583
|
+
state: a.region,
|
|
584
|
+
zip: a.postalCode,
|
|
585
|
+
country: a.country,
|
|
586
|
+
type: spectrumSimpleTypeToWa(a.type)
|
|
587
|
+
})),
|
|
588
|
+
urls: (contact.urls ?? []).map((url) => ({ url })),
|
|
589
|
+
org: contact.org?.name || contact.org?.department || contact.org?.title ? {
|
|
590
|
+
company: contact.org.name,
|
|
591
|
+
department: contact.org.department,
|
|
592
|
+
title: contact.org.title
|
|
593
|
+
} : void 0,
|
|
594
|
+
birthday: contact.birthday
|
|
595
|
+
};
|
|
596
|
+
return card;
|
|
597
|
+
};
|
|
598
|
+
var clientStream = (client) => {
|
|
599
|
+
const eventStream = client.events.subscribe().filter(
|
|
600
|
+
(e) => e.type === "message"
|
|
601
|
+
);
|
|
602
|
+
return stream((emit, end) => {
|
|
603
|
+
const pump = (async () => {
|
|
604
|
+
try {
|
|
605
|
+
for await (const event of eventStream) {
|
|
606
|
+
for (const m of toMessages(client, event.message)) {
|
|
607
|
+
await emit(m);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
end();
|
|
611
|
+
} catch (e) {
|
|
612
|
+
end(e);
|
|
613
|
+
}
|
|
614
|
+
})();
|
|
615
|
+
return async () => {
|
|
616
|
+
await eventStream.close();
|
|
617
|
+
await pump;
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
var messages = (clients) => mergeStreams(clients.map(clientStream));
|
|
622
|
+
var send = async (clients, spaceId, content) => {
|
|
623
|
+
if (content.type === "reply") {
|
|
624
|
+
return await replyToMessage(
|
|
625
|
+
clients,
|
|
626
|
+
spaceId,
|
|
627
|
+
content.target.id,
|
|
628
|
+
content.content
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
if (content.type === "reaction") {
|
|
632
|
+
await reactToMessage(clients, spaceId, content.target.id, content.emoji);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (content.type === "typing") {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const client = primary(clients);
|
|
639
|
+
switch (content.type) {
|
|
640
|
+
case "text":
|
|
641
|
+
return toRecord(
|
|
642
|
+
await client.messages.send({ to: spaceId, text: content.text }),
|
|
643
|
+
spaceId,
|
|
644
|
+
content
|
|
645
|
+
);
|
|
646
|
+
case "attachment": {
|
|
647
|
+
const { mediaId } = await client.media.upload({
|
|
648
|
+
file: await content.read(),
|
|
649
|
+
mimeType: content.mimeType,
|
|
650
|
+
filename: content.name
|
|
651
|
+
});
|
|
652
|
+
const mediaType = mimeToMediaType(content.mimeType);
|
|
653
|
+
const mediaPayload = mediaType === "document" ? { id: mediaId, filename: content.name } : { id: mediaId };
|
|
654
|
+
return toRecord(
|
|
655
|
+
await client.messages.send({
|
|
656
|
+
to: spaceId,
|
|
657
|
+
[mediaType]: mediaPayload
|
|
658
|
+
}),
|
|
659
|
+
spaceId,
|
|
660
|
+
content
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
case "contact":
|
|
664
|
+
return toRecord(
|
|
665
|
+
await client.messages.send({
|
|
666
|
+
to: spaceId,
|
|
667
|
+
contacts: [contactToWa(content)]
|
|
668
|
+
}),
|
|
669
|
+
spaceId,
|
|
670
|
+
content
|
|
671
|
+
);
|
|
672
|
+
case "voice": {
|
|
673
|
+
const { mediaId } = await client.media.upload({
|
|
674
|
+
file: await content.read(),
|
|
675
|
+
mimeType: content.mimeType,
|
|
676
|
+
filename: voiceFilename(content)
|
|
677
|
+
});
|
|
678
|
+
return toRecord(
|
|
679
|
+
await client.messages.send({
|
|
680
|
+
to: spaceId,
|
|
681
|
+
audio: { id: mediaId }
|
|
682
|
+
}),
|
|
683
|
+
spaceId,
|
|
684
|
+
content
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
case "poll": {
|
|
688
|
+
const result = await client.messages.send({
|
|
689
|
+
to: spaceId,
|
|
690
|
+
interactive: pollToInteractive(content)
|
|
691
|
+
});
|
|
692
|
+
cachePoll(client, result.messageId, content);
|
|
693
|
+
return toRecord(result, spaceId, content);
|
|
694
|
+
}
|
|
695
|
+
default:
|
|
696
|
+
throw UnsupportedError.content(content.type);
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
var reactToMessage = async (clients, spaceId, messageId, reaction) => {
|
|
700
|
+
await primary(clients).messages.send({
|
|
701
|
+
to: spaceId,
|
|
702
|
+
reaction: { messageId, emoji: reaction }
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
var replyToMessage = async (clients, spaceId, messageId, content) => {
|
|
706
|
+
const client = primary(clients);
|
|
707
|
+
switch (content.type) {
|
|
708
|
+
case "text":
|
|
709
|
+
return toRecord(
|
|
710
|
+
await client.messages.send({
|
|
711
|
+
to: spaceId,
|
|
712
|
+
replyTo: messageId,
|
|
713
|
+
text: content.text
|
|
714
|
+
}),
|
|
715
|
+
spaceId,
|
|
716
|
+
content
|
|
717
|
+
);
|
|
718
|
+
case "attachment": {
|
|
719
|
+
const { mediaId } = await client.media.upload({
|
|
720
|
+
file: await content.read(),
|
|
721
|
+
mimeType: content.mimeType,
|
|
722
|
+
filename: content.name
|
|
723
|
+
});
|
|
724
|
+
const mediaType = mimeToMediaType(content.mimeType);
|
|
725
|
+
const mediaPayload = mediaType === "document" ? { id: mediaId, filename: content.name } : { id: mediaId };
|
|
726
|
+
return toRecord(
|
|
727
|
+
await client.messages.send({
|
|
728
|
+
to: spaceId,
|
|
729
|
+
replyTo: messageId,
|
|
730
|
+
[mediaType]: mediaPayload
|
|
731
|
+
}),
|
|
732
|
+
spaceId,
|
|
733
|
+
content
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
case "contact":
|
|
737
|
+
return toRecord(
|
|
738
|
+
await client.messages.send({
|
|
739
|
+
to: spaceId,
|
|
740
|
+
replyTo: messageId,
|
|
741
|
+
contacts: [contactToWa(content)]
|
|
742
|
+
}),
|
|
743
|
+
spaceId,
|
|
744
|
+
content
|
|
745
|
+
);
|
|
746
|
+
case "voice": {
|
|
747
|
+
const { mediaId } = await client.media.upload({
|
|
748
|
+
file: await content.read(),
|
|
749
|
+
mimeType: content.mimeType,
|
|
750
|
+
filename: voiceFilename(content)
|
|
751
|
+
});
|
|
752
|
+
return toRecord(
|
|
753
|
+
await client.messages.send({
|
|
754
|
+
to: spaceId,
|
|
755
|
+
replyTo: messageId,
|
|
756
|
+
audio: { id: mediaId }
|
|
757
|
+
}),
|
|
758
|
+
spaceId,
|
|
759
|
+
content
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
case "poll": {
|
|
763
|
+
const result = await client.messages.send({
|
|
764
|
+
to: spaceId,
|
|
765
|
+
replyTo: messageId,
|
|
766
|
+
interactive: pollToInteractive(content)
|
|
767
|
+
});
|
|
768
|
+
cachePoll(client, result.messageId, content);
|
|
769
|
+
return toRecord(result, spaceId, content);
|
|
770
|
+
}
|
|
771
|
+
default:
|
|
772
|
+
throw UnsupportedError.content(content.type);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// src/providers/whatsapp-business/types.ts
|
|
777
|
+
import z from "zod";
|
|
778
|
+
var directConfig = z.object({
|
|
779
|
+
accessToken: z.string().min(1),
|
|
780
|
+
appSecret: z.string().optional(),
|
|
781
|
+
phoneNumberId: z.string().min(1)
|
|
782
|
+
});
|
|
783
|
+
var cloudConfig = z.object({}).strict();
|
|
784
|
+
var configSchema = z.union([directConfig, cloudConfig]);
|
|
785
|
+
var isCloudConfig = (config) => !("accessToken" in config);
|
|
786
|
+
var userSchema = z.object({});
|
|
787
|
+
var spaceSchema = z.object({
|
|
788
|
+
id: z.string()
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// src/providers/whatsapp-business/index.ts
|
|
792
|
+
var whatsappBusiness = definePlatform("WhatsApp Business", {
|
|
793
|
+
config: configSchema,
|
|
794
|
+
lifecycle: {
|
|
795
|
+
createClient: async ({
|
|
796
|
+
config,
|
|
797
|
+
projectId,
|
|
798
|
+
projectSecret
|
|
799
|
+
}) => {
|
|
800
|
+
if (!isCloudConfig(config)) {
|
|
801
|
+
return [
|
|
802
|
+
createClient2({
|
|
803
|
+
accessToken: config.accessToken,
|
|
804
|
+
appSecret: config.appSecret ?? "",
|
|
805
|
+
phoneNumberId: config.phoneNumberId
|
|
806
|
+
})
|
|
807
|
+
];
|
|
808
|
+
}
|
|
809
|
+
if (!(projectId && projectSecret)) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
"WhatsApp Business cloud mode requires projectId and projectSecret. Either pass credentials to Spectrum(), or provide direct credentials: whatsappBusiness.config({ accessToken, phoneNumberId })"
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return await createCloudClients(projectId, projectSecret);
|
|
815
|
+
},
|
|
816
|
+
destroyClient: async ({ client }) => {
|
|
817
|
+
await disposeCloudAuth(client);
|
|
818
|
+
await Promise.all(client.map((c) => c.close()));
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
user: {
|
|
822
|
+
resolve: async ({ input }) => ({ id: input.userID })
|
|
823
|
+
},
|
|
824
|
+
space: {
|
|
825
|
+
schema: spaceSchema,
|
|
826
|
+
resolve: async ({ input }) => {
|
|
827
|
+
if (input.users.length === 0) {
|
|
828
|
+
throw new Error("WhatsApp space creation requires at least one user");
|
|
829
|
+
}
|
|
830
|
+
if (input.users.length > 1) {
|
|
831
|
+
throw UnsupportedError.action(
|
|
832
|
+
"createSpace",
|
|
833
|
+
"WhatsApp Business",
|
|
834
|
+
"only 1:1 conversations are supported"
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const user = input.users[0];
|
|
838
|
+
if (!user) {
|
|
839
|
+
throw new Error("WhatsApp space creation requires a user");
|
|
840
|
+
}
|
|
841
|
+
return { id: user.id };
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
messages: ({ client }) => messages(client),
|
|
845
|
+
send: async ({ space, content, client }) => await send(client, space.id, content)
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
export {
|
|
849
|
+
whatsappBusiness
|
|
850
|
+
};
|