@spectrum-ts/whatsapp-business 11.2.0 → 12.1.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/index.d.ts +45 -3
- package/dist/index.js +191 -76
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WhatsAppClient } from "@photon-ai/whatsapp-business";
|
|
2
|
-
import { SchemaMessage } from "@spectrum-ts/core";
|
|
2
|
+
import { ContentBuilder, SchemaMessage } from "@spectrum-ts/core";
|
|
3
|
+
import { ProviderMessageRecord } from "@spectrum-ts/core/authoring";
|
|
3
4
|
import z from "zod";
|
|
4
5
|
|
|
5
6
|
//#region src/types.d.ts
|
|
@@ -10,8 +11,49 @@ declare const spaceSchema: z.ZodObject<{
|
|
|
10
11
|
}, z.core.$strip>;
|
|
11
12
|
type WhatsAppMessage = SchemaMessage<typeof userSchema, typeof spaceSchema>;
|
|
12
13
|
//#endregion
|
|
14
|
+
//#region src/content/template.d.ts
|
|
15
|
+
/**
|
|
16
|
+
* WhatsApp-only template message content. Lives entirely under the WhatsApp
|
|
17
|
+
* Business provider — never enters the universal `Content` discriminated
|
|
18
|
+
* union. The framework recognizes it via the generic content-level platform
|
|
19
|
+
* contract: `__platform: "whatsapp_business"` lets
|
|
20
|
+
* `findUnsupportedPlatformContent` warn-and-skip when a different platform
|
|
21
|
+
* receives it.
|
|
22
|
+
*
|
|
23
|
+
* Templates are the only message type Meta accepts outside the 24-hour
|
|
24
|
+
* customer-service window (free-form sends fail with error 131047), so this
|
|
25
|
+
* is the escape hatch for re-engaging stale conversations. v1 covers the
|
|
26
|
+
* common case: a pre-approved template addressed by name + language, with
|
|
27
|
+
* positional `{{1}}..{{n}}` body text parameters. Header/button components
|
|
28
|
+
* can be added later without breaking this shape.
|
|
29
|
+
*/
|
|
30
|
+
declare const whatsappTemplateSchema: z.ZodObject<{
|
|
31
|
+
type: z.ZodLiteral<"whatsapp-template">;
|
|
32
|
+
__platform: z.ZodLiteral<"whatsapp_business">;
|
|
33
|
+
name: z.ZodString;
|
|
34
|
+
languageCode: z.ZodString;
|
|
35
|
+
bodyParams: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
type WhatsAppTemplate = z.infer<typeof whatsappTemplateSchema>;
|
|
38
|
+
type WhatsAppTemplateInput = Omit<WhatsAppTemplate, "type" | "__platform">;
|
|
39
|
+
declare const isWhatsAppTemplate: (value: unknown) => value is WhatsAppTemplate;
|
|
40
|
+
declare const asWhatsAppTemplate: (input: WhatsAppTemplateInput) => WhatsAppTemplate;
|
|
41
|
+
declare function whatsappTemplate(input: WhatsAppTemplateInput): ContentBuilder;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/errors/partial-send.d.ts
|
|
44
|
+
declare class WhatsAppPartialSendError extends Error {
|
|
45
|
+
readonly sent: ProviderMessageRecord[];
|
|
46
|
+
readonly failedIndex: number;
|
|
47
|
+
constructor(input: {
|
|
48
|
+
sent: ProviderMessageRecord[];
|
|
49
|
+
failedIndex: number;
|
|
50
|
+
total: number;
|
|
51
|
+
cause: unknown;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
13
55
|
//#region src/index.d.ts
|
|
14
|
-
declare const whatsappBusiness: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"
|
|
56
|
+
declare const whatsappBusiness: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"whatsapp_business", import("zod").ZodUnion<readonly [import("zod").ZodObject<{
|
|
15
57
|
accessToken: import("zod").ZodString;
|
|
16
58
|
appSecret: import("zod").ZodOptional<import("zod").ZodString>;
|
|
17
59
|
phoneNumberId: import("zod").ZodString;
|
|
@@ -23,4 +65,4 @@ declare const whatsappBusiness: import("@spectrum-ts/core").Platform<import("@sp
|
|
|
23
65
|
id: string;
|
|
24
66
|
}, undefined, WhatsAppMessage, undefined, Record<never, never>, Record<never, never>, Record<never, never>>> & Readonly<Record<never, never>>;
|
|
25
67
|
//#endregion
|
|
26
|
-
export { whatsappBusiness };
|
|
68
|
+
export { WhatsAppPartialSendError, type WhatsAppTemplate, type WhatsAppTemplateInput, asWhatsAppTemplate, isWhatsAppTemplate, whatsappBusiness, whatsappTemplate, whatsappTemplateSchema };
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { TypedEventStream, button, buttons, createClient, list } from "@photon-ai/whatsapp-business";
|
|
2
2
|
import { UnsupportedError, cloud, definePlatform, mergeStreams, stream } from "@spectrum-ts/core";
|
|
3
|
-
import { asAttachment, asContact, asCustom, asGroup, asPollOption, asReaction, asText, asUnsend, createLogger, errorAttrs, tracedFetch } from "@spectrum-ts/core/authoring";
|
|
3
|
+
import { asAttachment, asContact, asCustom, asGroup, asPollOption, asReaction, asReply, asText, asUnsend, asVoice, createLogger, createTokenRenewal, errorAttrs, tracedFetch } from "@spectrum-ts/core/authoring";
|
|
4
4
|
import { extension } from "mime-types";
|
|
5
5
|
import z from "zod";
|
|
6
6
|
//#region src/auth.ts
|
|
7
|
-
const log = createLogger("spectrum.whatsapp.auth");
|
|
8
7
|
const streamLog$1 = createLogger("spectrum.whatsapp.stream");
|
|
9
|
-
const RENEWAL_RATIO = .8;
|
|
10
|
-
const EXPIRY_BUFFER_MS = 3e4;
|
|
11
|
-
const RETRY_DELAY_MS = 3e4;
|
|
12
8
|
const RESUBSCRIBE_BACKOFF_MS = 500;
|
|
13
9
|
const ignoreCleanupError = () => void 0;
|
|
14
10
|
const cloudAuthState = /* @__PURE__ */ new WeakMap();
|
|
15
11
|
async function createCloudClients(projectId, projectSecret) {
|
|
16
12
|
let tokenData = await cloud.issueWhatsappBusinessTokens(projectId, projectSecret);
|
|
17
|
-
let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
18
|
-
let disposed = false;
|
|
19
|
-
let renewalTimer;
|
|
20
|
-
let refreshFailures = 0;
|
|
21
|
-
let refreshInFlight;
|
|
22
13
|
const lines = /* @__PURE__ */ new Map();
|
|
23
14
|
const buildRawClient = (phoneNumberId) => {
|
|
24
15
|
const accessToken = tokenData.auth[phoneNumberId];
|
|
@@ -31,7 +22,6 @@ async function createCloudClients(projectId, projectSecret) {
|
|
|
31
22
|
};
|
|
32
23
|
const refreshTokens = async () => {
|
|
33
24
|
tokenData = await cloud.issueWhatsappBusinessTokens(projectId, projectSecret);
|
|
34
|
-
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
35
25
|
for (const [phoneNumberId, state] of lines) {
|
|
36
26
|
if (!tokenData.auth[phoneNumberId]) continue;
|
|
37
27
|
const old = state.current;
|
|
@@ -40,69 +30,21 @@ async function createCloudClients(projectId, projectSecret) {
|
|
|
40
30
|
await old.close().catch(() => void 0);
|
|
41
31
|
}
|
|
42
32
|
};
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
49
|
-
const onRefreshFailure = (error) => {
|
|
50
|
-
refreshFailures += 1;
|
|
51
|
-
log.warn("whatsapp token refresh failed; retrying", {
|
|
52
|
-
"spectrum.whatsapp.auth.attempt": refreshFailures,
|
|
53
|
-
"spectrum.whatsapp.auth.retry_in_ms": RETRY_DELAY_MS,
|
|
54
|
-
...errorAttrs(error)
|
|
55
|
-
}, error);
|
|
56
|
-
};
|
|
57
|
-
const clearRenewalTimer = () => {
|
|
58
|
-
if (renewalTimer !== void 0) {
|
|
59
|
-
clearTimeout(renewalTimer);
|
|
60
|
-
renewalTimer = void 0;
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
const refreshNow = async () => {
|
|
64
|
-
await refreshTokens();
|
|
65
|
-
onRefreshSuccess();
|
|
66
|
-
scheduleRenewal();
|
|
67
|
-
};
|
|
68
|
-
const coalescedRefresh = () => {
|
|
69
|
-
if (!refreshInFlight) refreshInFlight = refreshNow().finally(() => {
|
|
70
|
-
refreshInFlight = void 0;
|
|
71
|
-
});
|
|
72
|
-
return refreshInFlight;
|
|
73
|
-
};
|
|
74
|
-
const scheduleRenewal = () => {
|
|
75
|
-
if (disposed) return;
|
|
76
|
-
clearRenewalTimer();
|
|
77
|
-
const ttlMs = tokenData.expiresIn * 1e3;
|
|
78
|
-
const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
|
|
79
|
-
const runScheduledRefresh = () => {
|
|
80
|
-
coalescedRefresh().catch((err) => {
|
|
81
|
-
onRefreshFailure(err);
|
|
82
|
-
if (disposed) return;
|
|
83
|
-
renewalTimer = setTimeout(runScheduledRefresh, RETRY_DELAY_MS);
|
|
84
|
-
renewalTimer?.unref?.();
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
renewalTimer = setTimeout(runScheduledRefresh, renewInMs);
|
|
88
|
-
renewalTimer?.unref?.();
|
|
89
|
-
};
|
|
90
|
-
const refreshIfNeeded = async () => {
|
|
91
|
-
if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) return;
|
|
92
|
-
await coalescedRefresh();
|
|
93
|
-
};
|
|
94
|
-
scheduleRenewal();
|
|
33
|
+
const renewal = createTokenRenewal({
|
|
34
|
+
expiresInSeconds: () => tokenData.expiresIn,
|
|
35
|
+
name: "whatsapp",
|
|
36
|
+
refresh: refreshTokens
|
|
37
|
+
});
|
|
95
38
|
const clients = Object.keys(tokenData.auth).map((phoneNumberId) => {
|
|
96
39
|
const state = {
|
|
97
40
|
current: buildRawClient(phoneNumberId),
|
|
98
41
|
subscriptions: /* @__PURE__ */ new Set()
|
|
99
42
|
};
|
|
100
43
|
lines.set(phoneNumberId, state);
|
|
101
|
-
return buildClientProxy(state, refreshIfNeeded);
|
|
44
|
+
return buildClientProxy(state, renewal.refreshIfNeeded);
|
|
102
45
|
});
|
|
103
46
|
cloudAuthState.set(clients, { dispose: async () => {
|
|
104
|
-
|
|
105
|
-
clearRenewalTimer();
|
|
47
|
+
renewal.dispose();
|
|
106
48
|
for (const state of lines.values()) for (const sub of state.subscriptions) sub.close();
|
|
107
49
|
await Promise.allSettled(Array.from(lines.values()).map((s) => s.current.close()));
|
|
108
50
|
lines.clear();
|
|
@@ -251,6 +193,51 @@ const resubscribableStream = (state, options) => {
|
|
|
251
193
|
});
|
|
252
194
|
};
|
|
253
195
|
//#endregion
|
|
196
|
+
//#region src/content/template.ts
|
|
197
|
+
/**
|
|
198
|
+
* WhatsApp-only template message content. Lives entirely under the WhatsApp
|
|
199
|
+
* Business provider — never enters the universal `Content` discriminated
|
|
200
|
+
* union. The framework recognizes it via the generic content-level platform
|
|
201
|
+
* contract: `__platform: "whatsapp_business"` lets
|
|
202
|
+
* `findUnsupportedPlatformContent` warn-and-skip when a different platform
|
|
203
|
+
* receives it.
|
|
204
|
+
*
|
|
205
|
+
* Templates are the only message type Meta accepts outside the 24-hour
|
|
206
|
+
* customer-service window (free-form sends fail with error 131047), so this
|
|
207
|
+
* is the escape hatch for re-engaging stale conversations. v1 covers the
|
|
208
|
+
* common case: a pre-approved template addressed by name + language, with
|
|
209
|
+
* positional `{{1}}..{{n}}` body text parameters. Header/button components
|
|
210
|
+
* can be added later without breaking this shape.
|
|
211
|
+
*/
|
|
212
|
+
const whatsappTemplateSchema = z.object({
|
|
213
|
+
type: z.literal("whatsapp-template"),
|
|
214
|
+
__platform: z.literal("whatsapp_business"),
|
|
215
|
+
name: z.string().min(1),
|
|
216
|
+
languageCode: z.string().min(1),
|
|
217
|
+
bodyParams: z.array(z.string()).optional()
|
|
218
|
+
});
|
|
219
|
+
const isWhatsAppTemplate = (value) => whatsappTemplateSchema.safeParse(value).success;
|
|
220
|
+
const asWhatsAppTemplate = (input) => whatsappTemplateSchema.parse({
|
|
221
|
+
type: "whatsapp-template",
|
|
222
|
+
__platform: "whatsapp_business",
|
|
223
|
+
...input
|
|
224
|
+
});
|
|
225
|
+
function whatsappTemplate(input) {
|
|
226
|
+
return { build: async () => asWhatsAppTemplate(input) };
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/errors/partial-send.ts
|
|
230
|
+
var WhatsAppPartialSendError = class extends Error {
|
|
231
|
+
sent;
|
|
232
|
+
failedIndex;
|
|
233
|
+
constructor(input) {
|
|
234
|
+
super(`WhatsApp group send failed at part ${input.failedIndex + 1}/${input.total}; ${input.sent.length} earlier part(s) were already delivered and cannot be retracted`, { cause: input.cause });
|
|
235
|
+
this.name = "WhatsAppPartialSendError";
|
|
236
|
+
this.sent = input.sent;
|
|
237
|
+
this.failedIndex = input.failedIndex;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
254
241
|
//#region src/poll.ts
|
|
255
242
|
const MAX_BUTTON_OPTIONS = 3;
|
|
256
243
|
const LIST_BUTTON_TEXT = "View options";
|
|
@@ -321,6 +308,26 @@ const reactionTargetStub = (reactedId) => ({
|
|
|
321
308
|
stub: true
|
|
322
309
|
})
|
|
323
310
|
});
|
|
311
|
+
const replyTargetStub = (quotedId) => ({
|
|
312
|
+
id: quotedId,
|
|
313
|
+
content: asCustom({
|
|
314
|
+
whatsapp_type: "reply-target",
|
|
315
|
+
stub: true
|
|
316
|
+
})
|
|
317
|
+
});
|
|
318
|
+
const REPLY_EXEMPT_TYPES = new Set([
|
|
319
|
+
"interactive",
|
|
320
|
+
"button",
|
|
321
|
+
"reaction"
|
|
322
|
+
]);
|
|
323
|
+
const withReplyContext = (msg, content) => {
|
|
324
|
+
const quotedId = msg.context?.id;
|
|
325
|
+
if (quotedId === void 0 || REPLY_EXEMPT_TYPES.has(msg.content.type)) return content;
|
|
326
|
+
return asReply({
|
|
327
|
+
content,
|
|
328
|
+
target: replyTargetStub(quotedId)
|
|
329
|
+
});
|
|
330
|
+
};
|
|
324
331
|
const optionIndexFromId = (id) => {
|
|
325
332
|
if (!id.startsWith(OPTION_ID_PREFIX)) return;
|
|
326
333
|
const index = Number(id.slice(4));
|
|
@@ -411,13 +418,13 @@ const toMessages = (client, msg) => {
|
|
|
411
418
|
return msg.content.contacts.map((card, index) => ({
|
|
412
419
|
...base,
|
|
413
420
|
id: multi ? `${msg.id}:${index}` : msg.id,
|
|
414
|
-
content: waContactToSpectrum(card)
|
|
421
|
+
content: withReplyContext(msg, waContactToSpectrum(card))
|
|
415
422
|
}));
|
|
416
423
|
}
|
|
417
424
|
return [{
|
|
418
425
|
...base,
|
|
419
426
|
id: msg.id,
|
|
420
|
-
content: mapContent(client, msg)
|
|
427
|
+
content: withReplyContext(msg, mapContent(client, msg))
|
|
421
428
|
}];
|
|
422
429
|
};
|
|
423
430
|
const mapReactionContent = (client, msg, reaction) => {
|
|
@@ -456,7 +463,7 @@ const mapContent = (client, msg) => {
|
|
|
456
463
|
case "video":
|
|
457
464
|
case "audio":
|
|
458
465
|
case "document": {
|
|
459
|
-
const media = lazyMedia(client, content.media);
|
|
466
|
+
const media = content.type === "audio" && content.media.voice ? lazyVoice(client, content.media) : lazyMedia(client, content.media);
|
|
460
467
|
const caption = content.media.caption?.trim();
|
|
461
468
|
if (!caption) return media;
|
|
462
469
|
return asGroup({ items: [groupItem(msg, 0, media), groupItem(msg, 1, asText(caption))] });
|
|
@@ -518,7 +525,7 @@ const fetchMedia = async (client, mediaId) => {
|
|
|
518
525
|
if (!response.ok) throw new Error(`Media download failed: ${response.status}`);
|
|
519
526
|
return response;
|
|
520
527
|
};
|
|
521
|
-
const
|
|
528
|
+
const lazyMediaSource = (client, media) => ({
|
|
522
529
|
id: media.id,
|
|
523
530
|
name: media.filename ?? `media-${media.id}`,
|
|
524
531
|
mimeType: media.mimeType,
|
|
@@ -529,6 +536,8 @@ const lazyMedia = (client, media) => asAttachment({
|
|
|
529
536
|
return response.body;
|
|
530
537
|
}
|
|
531
538
|
});
|
|
539
|
+
const lazyMedia = (client, media) => asAttachment(lazyMediaSource(client, media));
|
|
540
|
+
const lazyVoice = (client, media) => asVoice(lazyMediaSource(client, media));
|
|
532
541
|
const mimeToMediaType = (mimeType) => {
|
|
533
542
|
if (mimeType.startsWith("image/")) return "image";
|
|
534
543
|
if (mimeType.startsWith("video/")) return "video";
|
|
@@ -626,7 +635,83 @@ const clientStream = (client) => {
|
|
|
626
635
|
});
|
|
627
636
|
};
|
|
628
637
|
const messages = (clients) => mergeStreams(clients.map(clientStream));
|
|
638
|
+
const MAX_CAPTION_LENGTH = 1024;
|
|
639
|
+
const captionablePair = (group) => {
|
|
640
|
+
if (group.items.length !== 2) return;
|
|
641
|
+
const contents = group.items.map((item) => item.content);
|
|
642
|
+
const attachment = contents.find((c) => c.type === "attachment");
|
|
643
|
+
const text = contents.find((c) => c.type === "text");
|
|
644
|
+
return attachment && text && mimeToMediaType(attachment.mimeType) !== "audio" && text.text.length <= MAX_CAPTION_LENGTH ? {
|
|
645
|
+
attachment,
|
|
646
|
+
caption: text.text
|
|
647
|
+
} : void 0;
|
|
648
|
+
};
|
|
649
|
+
const sendGroupParts = async (clients, spaceId, group, replyTo) => {
|
|
650
|
+
const pair = captionablePair(group);
|
|
651
|
+
if (pair) {
|
|
652
|
+
const client = primary(clients);
|
|
653
|
+
const { attachment, caption } = pair;
|
|
654
|
+
const { mediaId } = await client.media.upload({
|
|
655
|
+
file: await attachment.read(),
|
|
656
|
+
mimeType: attachment.mimeType,
|
|
657
|
+
filename: attachment.name
|
|
658
|
+
});
|
|
659
|
+
const mediaType = mimeToMediaType(attachment.mimeType);
|
|
660
|
+
const mediaPayload = mediaType === "document" ? {
|
|
661
|
+
id: mediaId,
|
|
662
|
+
filename: attachment.name,
|
|
663
|
+
caption
|
|
664
|
+
} : {
|
|
665
|
+
id: mediaId,
|
|
666
|
+
caption
|
|
667
|
+
};
|
|
668
|
+
return toRecord(await client.messages.send({
|
|
669
|
+
to: spaceId,
|
|
670
|
+
...replyTo === void 0 ? {} : { replyTo },
|
|
671
|
+
[mediaType]: mediaPayload
|
|
672
|
+
}), spaceId, group);
|
|
673
|
+
}
|
|
674
|
+
const sent = [];
|
|
675
|
+
let first;
|
|
676
|
+
for (const [index, item] of group.items.entries()) {
|
|
677
|
+
const itemContent = item.content;
|
|
678
|
+
let record;
|
|
679
|
+
try {
|
|
680
|
+
record = index === 0 && replyTo !== void 0 ? await replyToMessage(clients, spaceId, replyTo, itemContent) : await send(clients, spaceId, itemContent);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (sent.length === 0) throw error;
|
|
683
|
+
throw new WhatsAppPartialSendError({
|
|
684
|
+
sent,
|
|
685
|
+
failedIndex: index,
|
|
686
|
+
total: group.items.length,
|
|
687
|
+
cause: error
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (record) sent.push(record);
|
|
691
|
+
first ??= record;
|
|
692
|
+
}
|
|
693
|
+
if (!first) throw UnsupportedError.content(group.type);
|
|
694
|
+
return {
|
|
695
|
+
...first,
|
|
696
|
+
content: group
|
|
697
|
+
};
|
|
698
|
+
};
|
|
699
|
+
const toTemplateInput = (content) => ({
|
|
700
|
+
name: content.name,
|
|
701
|
+
languageCode: content.languageCode,
|
|
702
|
+
components: content.bodyParams?.length ? [{
|
|
703
|
+
type: "body",
|
|
704
|
+
parameters: content.bodyParams.map((text) => ({
|
|
705
|
+
type: "text",
|
|
706
|
+
text
|
|
707
|
+
}))
|
|
708
|
+
}] : []
|
|
709
|
+
});
|
|
629
710
|
const send = async (clients, spaceId, content) => {
|
|
711
|
+
if (isWhatsAppTemplate(content)) return toRecord(await primary(clients).messages.send({
|
|
712
|
+
to: spaceId,
|
|
713
|
+
template: toTemplateInput(content)
|
|
714
|
+
}), spaceId, content);
|
|
630
715
|
if (content.type === "reply") return await replyToMessage(clients, spaceId, parentWamid(content.target.id), content.content);
|
|
631
716
|
if (content.type === "reaction") return await reactToMessage(clients, spaceId, content);
|
|
632
717
|
if (content.type === "typing") return;
|
|
@@ -691,10 +776,21 @@ const send = async (clients, spaceId, content) => {
|
|
|
691
776
|
cachePoll(client, result.messageId, content);
|
|
692
777
|
return toRecord(result, spaceId, content);
|
|
693
778
|
}
|
|
779
|
+
case "richlink": return toRecord(await client.messages.send({
|
|
780
|
+
to: spaceId,
|
|
781
|
+
text: {
|
|
782
|
+
body: content.url,
|
|
783
|
+
previewUrl: true
|
|
784
|
+
}
|
|
785
|
+
}), spaceId, content);
|
|
694
786
|
case "app": return toRecord(await client.messages.send({
|
|
695
787
|
to: spaceId,
|
|
696
|
-
text:
|
|
788
|
+
text: {
|
|
789
|
+
body: await content.url(),
|
|
790
|
+
previewUrl: true
|
|
791
|
+
}
|
|
697
792
|
}), spaceId, content);
|
|
793
|
+
case "group": return await sendGroupParts(clients, spaceId, content);
|
|
698
794
|
default: throw UnsupportedError.content(content.type);
|
|
699
795
|
}
|
|
700
796
|
};
|
|
@@ -709,6 +805,11 @@ const reactToMessage = async (clients, spaceId, content) => {
|
|
|
709
805
|
};
|
|
710
806
|
const replyToMessage = async (clients, spaceId, messageId, content) => {
|
|
711
807
|
const client = primary(clients);
|
|
808
|
+
if (isWhatsAppTemplate(content)) return toRecord(await client.messages.send({
|
|
809
|
+
to: spaceId,
|
|
810
|
+
replyTo: messageId,
|
|
811
|
+
template: toTemplateInput(content)
|
|
812
|
+
}), spaceId, content);
|
|
712
813
|
switch (content.type) {
|
|
713
814
|
case "text": return toRecord(await client.messages.send({
|
|
714
815
|
to: spaceId,
|
|
@@ -758,11 +859,23 @@ const replyToMessage = async (clients, spaceId, messageId, content) => {
|
|
|
758
859
|
cachePoll(client, result.messageId, content);
|
|
759
860
|
return toRecord(result, spaceId, content);
|
|
760
861
|
}
|
|
862
|
+
case "richlink": return toRecord(await client.messages.send({
|
|
863
|
+
to: spaceId,
|
|
864
|
+
replyTo: messageId,
|
|
865
|
+
text: {
|
|
866
|
+
body: content.url,
|
|
867
|
+
previewUrl: true
|
|
868
|
+
}
|
|
869
|
+
}), spaceId, content);
|
|
761
870
|
case "app": return toRecord(await client.messages.send({
|
|
762
871
|
to: spaceId,
|
|
763
872
|
replyTo: messageId,
|
|
764
|
-
text:
|
|
873
|
+
text: {
|
|
874
|
+
body: await content.url(),
|
|
875
|
+
previewUrl: true
|
|
876
|
+
}
|
|
765
877
|
}), spaceId, content);
|
|
878
|
+
case "group": return await sendGroupParts(clients, spaceId, content, messageId);
|
|
766
879
|
default: throw UnsupportedError.content(content.type);
|
|
767
880
|
}
|
|
768
881
|
};
|
|
@@ -777,9 +890,11 @@ const cloudConfig = z.object({}).strict();
|
|
|
777
890
|
const configSchema = z.union([directConfig, cloudConfig]);
|
|
778
891
|
const isCloudConfig = (config) => !("accessToken" in config);
|
|
779
892
|
z.object({});
|
|
893
|
+
const spaceSchema = z.object({ id: z.string() });
|
|
780
894
|
//#endregion
|
|
781
895
|
//#region src/index.ts
|
|
782
|
-
const
|
|
896
|
+
const PLATFORM_ID = "whatsapp_business";
|
|
897
|
+
const whatsappBusiness = definePlatform(PLATFORM_ID, {
|
|
783
898
|
config: configSchema,
|
|
784
899
|
lifecycle: {
|
|
785
900
|
createClient: async ({ config, projectId, projectSecret }) => {
|
|
@@ -798,10 +913,10 @@ const whatsappBusiness = definePlatform("WhatsApp Business", {
|
|
|
798
913
|
},
|
|
799
914
|
user: { resolve: async ({ input }) => ({ id: input.userID }) },
|
|
800
915
|
space: {
|
|
801
|
-
schema:
|
|
916
|
+
schema: spaceSchema,
|
|
802
917
|
create: async ({ input }) => {
|
|
803
918
|
if (input.users.length === 0) throw new Error("WhatsApp space creation requires at least one user");
|
|
804
|
-
if (input.users.length > 1) throw UnsupportedError.action("space.create",
|
|
919
|
+
if (input.users.length > 1) throw UnsupportedError.action("space.create", PLATFORM_ID, "only 1:1 conversations are supported");
|
|
805
920
|
const user = input.users[0];
|
|
806
921
|
if (!user) throw new Error("WhatsApp space creation requires a user");
|
|
807
922
|
return { id: user.id };
|
|
@@ -811,4 +926,4 @@ const whatsappBusiness = definePlatform("WhatsApp Business", {
|
|
|
811
926
|
send: async ({ space, content, client }) => await send(client, space.id, content)
|
|
812
927
|
});
|
|
813
928
|
//#endregion
|
|
814
|
-
export { whatsappBusiness };
|
|
929
|
+
export { WhatsAppPartialSendError, asWhatsAppTemplate, isWhatsAppTemplate, whatsappBusiness, whatsappTemplate, whatsappTemplateSchema };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectrum-ts/whatsapp-business",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.1.0",
|
|
4
4
|
"description": "WhatsApp Business provider for spectrum-ts.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"spectrum": {
|
|
29
|
-
"key": "
|
|
29
|
+
"key": "whatsapp_business",
|
|
30
30
|
"import": "whatsappBusiness",
|
|
31
|
-
"label": "
|
|
31
|
+
"label": "whatsapp_business"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@photon-ai/whatsapp-business": "^0.2.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"zod": "^4.2.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@spectrum-ts/core": "^
|
|
39
|
+
"@spectrum-ts/core": "^12.0.0",
|
|
40
40
|
"typescript": "^5 || ^6.0.0"
|
|
41
41
|
},
|
|
42
42
|
"license": "MIT"
|