Skip to content

Commit 9c0882b

Browse files
authored
Merge pull request #264 from Secreto31126/tracker
Added biz_opaque_callback_data support
2 parents bc52cdc + 45b0974 commit 9c0882b

File tree

5 files changed

+94
-36
lines changed

5 files changed

+94
-36
lines changed

src/emitters.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,13 @@ export type OnMessageArgs = {
9090
*
9191
* @param response - The message to send as a reply
9292
* @param context - Wether to mention the current message, defaults to false
93+
* @param biz_opaque_callback_data - An arbitrary 256B string, useful for tracking
9394
* @returns WhatsAppAPI.sendMessage return value
9495
*/
9596
reply: (
9697
response: ClientMessage,
97-
context?: boolean
98+
context?: boolean,
99+
biz_opaque_callback_data?: string
98100
) => Promise<ServerMessageResponse | Response>;
99101
/**
100102
* The WhatsAppAPI instance that emitted the event
@@ -142,6 +144,10 @@ export type OnStatusArgs = {
142144
* The error object
143145
*/
144146
error?: ServerError;
147+
/**
148+
* Arbitrary string included in sent messages
149+
*/
150+
biz_opaque_callback_data?: string;
145151
/**
146152
* The raw data from the API
147153
*/

src/index.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,15 @@ export default class WhatsAppAPI {
204204
* @param to - The user's phone number
205205
* @param message - A Whatsapp message, built using the corresponding module for each type of message.
206206
* @param context - The message ID of the message to reply to
207+
* @param biz_opaque_callback_data - An arbitrary 256B string, useful for tracking (length not checked by the framework)
207208
* @returns The server response
208209
*/
209210
async sendMessage(
210211
phoneID: string,
211212
to: string,
212213
message: ClientMessage,
213-
context?: string
214+
context?: string,
215+
biz_opaque_callback_data?: string
214216
): Promise<ServerMessageResponse | Response> {
215217
const type = message._type;
216218

@@ -228,6 +230,8 @@ export default class WhatsAppAPI {
228230
message._build();
229231

230232
if (context) request.context = { message_id: context };
233+
if (biz_opaque_callback_data)
234+
request.biz_opaque_callback_data = biz_opaque_callback_data;
231235

232236
// Make the post request
233237
const promise = this.fetch(
@@ -784,12 +788,13 @@ export default class WhatsAppAPI {
784788
message,
785789
name,
786790
raw: data,
787-
reply: (response, context = false) =>
791+
reply: (response, context = false, biz_opaque_callback_data) =>
788792
this.sendMessage(
789793
phoneID,
790794
from,
791795
response,
792-
context ? message.id : undefined
796+
context ? message.id : undefined,
797+
biz_opaque_callback_data
793798
),
794799
Whatsapp: this
795800
};
@@ -804,6 +809,7 @@ export default class WhatsAppAPI {
804809
const conversation = statuses.conversation;
805810
const pricing = statuses.pricing;
806811
const error = statuses.errors?.[0];
812+
const biz_opaque_callback_data = statuses.biz_opaque_callback_data;
807813

808814
const args: OnStatusArgs = {
809815
phoneID,
@@ -813,6 +819,7 @@ export default class WhatsAppAPI {
813819
conversation,
814820
pricing,
815821
error,
822+
biz_opaque_callback_data,
816823
raw: data
817824
};
818825

src/types.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,9 @@ export type ClientMessageRequest = {
326326
*/
327327
to: string;
328328
/**
329-
* Undocumented, optional (the framework doesn't use it)
329+
* Currently you can only send messages to individuals
330330
*/
331-
recipient_type?: "individual";
331+
recipient_type: "individual";
332332
/**
333333
* The message to reply to
334334
*/
@@ -338,6 +338,15 @@ export type ClientMessageRequest = {
338338
*/
339339
message_id: string;
340340
};
341+
/**
342+
* An arbitrary 256B string, useful for tracking.
343+
*
344+
* Any app subscribed to the messages webhook field on the WhatsApp Business Account can get this string,
345+
* as it is included in statuses object within webhook payloads.
346+
*
347+
* Cloud API does not process this field, it just returns it as part of sent/delivered/read message webhooks.
348+
*/
349+
biz_opaque_callback_data?: string;
341350
} & (
342351
| {
343352
type: "text";
@@ -710,6 +719,7 @@ export type PostData = {
710719
status: ServerStatus;
711720
timestamp: string;
712721
recipient_id: string;
722+
biz_opaque_callback_data?: string;
713723
} & (
714724
| {
715725
conversation: ServerConversation;

test/index.test.cjs

+51-29
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ describe("WhatsAppAPI", function () {
388388
const user = "3";
389389
const id = "something_random";
390390
const context = "another_random_id";
391+
const tracker = "tracker";
391392

392393
const type = "text";
393394
const message = new Text("Hello world");
@@ -406,6 +407,11 @@ describe("WhatsAppAPI", function () {
406407
}
407408
};
408409

410+
const requestWithTracker = {
411+
...request,
412+
biz_opaque_callback_data: tracker
413+
};
414+
409415
const expectedResponse = {
410416
messaging_product: "whatsapp",
411417
contacts: [
@@ -478,6 +484,31 @@ describe("WhatsAppAPI", function () {
478484
deepEqual(response, expectedResponse);
479485
});
480486

487+
it("should be able to send with a tracker (biz_opaque_callback_data)", async function () {
488+
clientFacebook
489+
.intercept({
490+
path: `/${Whatsapp.v}/${bot}/messages`,
491+
method: "POST",
492+
headers: {
493+
Authorization: `Bearer ${token}`,
494+
"Content-Type": "application/json"
495+
},
496+
body: JSON.stringify(requestWithTracker)
497+
})
498+
.reply(200, expectedResponse)
499+
.times(1);
500+
501+
const response = await Whatsapp.sendMessage(
502+
bot,
503+
user,
504+
message,
505+
undefined,
506+
tracker
507+
);
508+
509+
deepEqual(response, expectedResponse);
510+
});
511+
481512
it("should return the raw fetch response if parsed is false", async function () {
482513
Whatsapp.parsed = false;
483514

@@ -1419,6 +1450,10 @@ describe("WhatsAppAPI", function () {
14191450
});
14201451

14211452
describe("Webhooks", function () {
1453+
function threw(i) {
1454+
return (e) => e === i;
1455+
}
1456+
14221457
describe("Get", function () {
14231458
const mode = "subscribe";
14241459
const challenge = "challenge";
@@ -1449,40 +1484,34 @@ describe("WhatsAppAPI", function () {
14491484
});
14501485

14511486
it("should throw 500 if webhookVerifyToken is not specified", function () {
1452-
const compare = (e) => e === 500;
1453-
14541487
delete Whatsapp.webhookVerifyToken;
14551488

14561489
throws(function () {
14571490
Whatsapp.get(params);
1458-
}, compare);
1491+
}, threw(500));
14591492
});
14601493

14611494
it("should throw 400 if the request is missing data", function () {
1462-
const compare = (e) => e === 400;
1463-
14641495
throws(function () {
14651496
Whatsapp.get({});
1466-
}, compare);
1497+
}, threw(400));
14671498

14681499
throws(function () {
14691500
Whatsapp.get({ "hub.mode": mode });
1470-
}, compare);
1501+
}, threw(400));
14711502

14721503
throws(function () {
14731504
Whatsapp.get({ "hub.verify_token": token });
1474-
}, compare);
1505+
}, threw(400));
14751506
});
14761507

14771508
it("should throw 403 if the verification tokens don't match", function () {
1478-
const compare = (e) => e === 403;
1479-
14801509
throws(function () {
14811510
Whatsapp.get(
14821511
{ ...params, "hub.verify_token": "wrong" },
14831512
token
14841513
);
1485-
}, compare);
1514+
}, threw(403));
14861515
});
14871516
});
14881517

@@ -1520,6 +1549,7 @@ describe("WhatsAppAPI", function () {
15201549
billable: true,
15211550
category: "business-initiated"
15221551
};
1552+
const biz_opaque_callback_data = "5";
15231553

15241554
const valid_message_mock = new MessageWebhookMock(
15251555
phoneID,
@@ -1533,7 +1563,8 @@ describe("WhatsAppAPI", function () {
15331563
status,
15341564
id,
15351565
conversation,
1536-
pricing
1566+
pricing,
1567+
biz_opaque_callback_data
15371568
);
15381569

15391570
const Whatsapp = new WhatsAppAPI({
@@ -1553,47 +1584,39 @@ describe("WhatsAppAPI", function () {
15531584
describe("Validation", function () {
15541585
describe("Secure truthy (default)", function () {
15551586
it("should throw 400 if rawBody is missing", function () {
1556-
const compare = (e) => e === 400;
1557-
1558-
rejects(Whatsapp.post(valid_message_mock), compare);
1587+
rejects(Whatsapp.post(valid_message_mock), threw(400));
15591588

15601589
rejects(
15611590
Whatsapp.post(valid_message_mock, undefined),
1562-
compare
1591+
threw(400)
15631592
);
15641593
});
15651594

15661595
it("should throw 401 if signature is missing", function () {
1567-
const compare = (e) => e === 401;
1568-
15691596
rejects(
15701597
Whatsapp.post(valid_message_mock, body),
1571-
compare
1598+
threw(401)
15721599
);
15731600

15741601
rejects(
15751602
Whatsapp.post(valid_message_mock, body, undefined),
1576-
compare
1603+
threw(401)
15771604
);
15781605
});
15791606

15801607
it("should throw 500 if appSecret is not specified", function () {
1581-
const compare = (e) => e === 500;
1582-
15831608
delete Whatsapp.appSecret;
15841609

15851610
rejects(
15861611
Whatsapp.post(valid_message_mock, body, signature),
1587-
compare
1612+
threw(500)
15881613
);
15891614
});
15901615

15911616
it("should throw 401 if the signature doesn't match the hash", function () {
1592-
const compare = (e) => e === 401;
1593-
15941617
rejects(
15951618
Whatsapp.post(valid_message_mock, body, "wrong"),
1596-
compare
1619+
threw(401)
15971620
);
15981621
});
15991622

@@ -1626,11 +1649,9 @@ describe("WhatsAppAPI", function () {
16261649
});
16271650

16281651
it("should throw 400 if the request isn't a valid WhatsApp Cloud API request (data.object)", function () {
1629-
const compare = (e) => e === 400;
1630-
16311652
Whatsapp.secure = false;
16321653

1633-
rejects(Whatsapp.post({}), compare);
1654+
rejects(Whatsapp.post({}), threw(400));
16341655
});
16351656
});
16361657

@@ -1776,6 +1797,7 @@ describe("WhatsAppAPI", function () {
17761797
id,
17771798
conversation,
17781799
pricing,
1800+
biz_opaque_callback_data,
17791801
raw: valid_status_mock
17801802
});
17811803
});

test/webhooks.mocks.cjs

+14-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,15 @@ class StatusWebhookMock {
5050
/**
5151
* Helper class to test the status post request, conditionally creating the object based on the available data
5252
*/
53-
constructor(phoneID, phone, status, messageID, conversation, pricing) {
53+
constructor(
54+
phoneID,
55+
phone,
56+
status,
57+
messageID,
58+
conversation,
59+
pricing,
60+
biz_opaque_callback_data
61+
) {
5462
this.object = "whatsapp_business_account";
5563
this.entry = [
5664
{
@@ -95,6 +103,11 @@ class StatusWebhookMock {
95103
this.entry[0].changes[0].value.statuses[0].pricing = pricing;
96104
}
97105

106+
if (biz_opaque_callback_data) {
107+
this.entry[0].changes[0].value.statuses[0].biz_opaque_callback_data =
108+
biz_opaque_callback_data;
109+
}
110+
98111
if (
99112
Object.keys(this.entry[0].changes[0].value.statuses[0]).length === 0
100113
) {

0 commit comments

Comments
 (0)