Skip to content

Commit

Permalink
Merge pull request #264 from Secreto31126/tracker
Browse files Browse the repository at this point in the history
Added biz_opaque_callback_data support
  • Loading branch information
Secreto31126 authored Nov 30, 2023
2 parents bc52cdc + 45b0974 commit 9c0882b
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 36 deletions.
8 changes: 7 additions & 1 deletion src/emitters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ export type OnMessageArgs = {
*
* @param response - The message to send as a reply
* @param context - Wether to mention the current message, defaults to false
* @param biz_opaque_callback_data - An arbitrary 256B string, useful for tracking
* @returns WhatsAppAPI.sendMessage return value
*/
reply: (
response: ClientMessage,
context?: boolean
context?: boolean,
biz_opaque_callback_data?: string
) => Promise<ServerMessageResponse | Response>;
/**
* The WhatsAppAPI instance that emitted the event
Expand Down Expand Up @@ -142,6 +144,10 @@ export type OnStatusArgs = {
* The error object
*/
error?: ServerError;
/**
* Arbitrary string included in sent messages
*/
biz_opaque_callback_data?: string;
/**
* The raw data from the API
*/
Expand Down
13 changes: 10 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,15 @@ export default class WhatsAppAPI {
* @param to - The user's phone number
* @param message - A Whatsapp message, built using the corresponding module for each type of message.
* @param context - The message ID of the message to reply to
* @param biz_opaque_callback_data - An arbitrary 256B string, useful for tracking (length not checked by the framework)
* @returns The server response
*/
async sendMessage(
phoneID: string,
to: string,
message: ClientMessage,
context?: string
context?: string,
biz_opaque_callback_data?: string
): Promise<ServerMessageResponse | Response> {
const type = message._type;

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

if (context) request.context = { message_id: context };
if (biz_opaque_callback_data)
request.biz_opaque_callback_data = biz_opaque_callback_data;

// Make the post request
const promise = this.fetch(
Expand Down Expand Up @@ -784,12 +788,13 @@ export default class WhatsAppAPI {
message,
name,
raw: data,
reply: (response, context = false) =>
reply: (response, context = false, biz_opaque_callback_data) =>
this.sendMessage(
phoneID,
from,
response,
context ? message.id : undefined
context ? message.id : undefined,
biz_opaque_callback_data
),
Whatsapp: this
};
Expand All @@ -804,6 +809,7 @@ export default class WhatsAppAPI {
const conversation = statuses.conversation;
const pricing = statuses.pricing;
const error = statuses.errors?.[0];
const biz_opaque_callback_data = statuses.biz_opaque_callback_data;

const args: OnStatusArgs = {
phoneID,
Expand All @@ -813,6 +819,7 @@ export default class WhatsAppAPI {
conversation,
pricing,
error,
biz_opaque_callback_data,
raw: data
};

Expand Down
14 changes: 12 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,9 @@ export type ClientMessageRequest = {
*/
to: string;
/**
* Undocumented, optional (the framework doesn't use it)
* Currently you can only send messages to individuals
*/
recipient_type?: "individual";
recipient_type: "individual";
/**
* The message to reply to
*/
Expand All @@ -338,6 +338,15 @@ export type ClientMessageRequest = {
*/
message_id: string;
};
/**
* An arbitrary 256B string, useful for tracking.
*
* Any app subscribed to the messages webhook field on the WhatsApp Business Account can get this string,
* as it is included in statuses object within webhook payloads.
*
* Cloud API does not process this field, it just returns it as part of sent/delivered/read message webhooks.
*/
biz_opaque_callback_data?: string;
} & (
| {
type: "text";
Expand Down Expand Up @@ -710,6 +719,7 @@ export type PostData = {
status: ServerStatus;
timestamp: string;
recipient_id: string;
biz_opaque_callback_data?: string;
} & (
| {
conversation: ServerConversation;
Expand Down
80 changes: 51 additions & 29 deletions test/index.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ describe("WhatsAppAPI", function () {
const user = "3";
const id = "something_random";
const context = "another_random_id";
const tracker = "tracker";

const type = "text";
const message = new Text("Hello world");
Expand All @@ -406,6 +407,11 @@ describe("WhatsAppAPI", function () {
}
};

const requestWithTracker = {
...request,
biz_opaque_callback_data: tracker
};

const expectedResponse = {
messaging_product: "whatsapp",
contacts: [
Expand Down Expand Up @@ -478,6 +484,31 @@ describe("WhatsAppAPI", function () {
deepEqual(response, expectedResponse);
});

it("should be able to send with a tracker (biz_opaque_callback_data)", async function () {
clientFacebook
.intercept({
path: `/${Whatsapp.v}/${bot}/messages`,
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(requestWithTracker)
})
.reply(200, expectedResponse)
.times(1);

const response = await Whatsapp.sendMessage(
bot,
user,
message,
undefined,
tracker
);

deepEqual(response, expectedResponse);
});

it("should return the raw fetch response if parsed is false", async function () {
Whatsapp.parsed = false;

Expand Down Expand Up @@ -1419,6 +1450,10 @@ describe("WhatsAppAPI", function () {
});

describe("Webhooks", function () {
function threw(i) {
return (e) => e === i;
}

describe("Get", function () {
const mode = "subscribe";
const challenge = "challenge";
Expand Down Expand Up @@ -1449,40 +1484,34 @@ describe("WhatsAppAPI", function () {
});

it("should throw 500 if webhookVerifyToken is not specified", function () {
const compare = (e) => e === 500;

delete Whatsapp.webhookVerifyToken;

throws(function () {
Whatsapp.get(params);
}, compare);
}, threw(500));
});

it("should throw 400 if the request is missing data", function () {
const compare = (e) => e === 400;

throws(function () {
Whatsapp.get({});
}, compare);
}, threw(400));

throws(function () {
Whatsapp.get({ "hub.mode": mode });
}, compare);
}, threw(400));

throws(function () {
Whatsapp.get({ "hub.verify_token": token });
}, compare);
}, threw(400));
});

it("should throw 403 if the verification tokens don't match", function () {
const compare = (e) => e === 403;

throws(function () {
Whatsapp.get(
{ ...params, "hub.verify_token": "wrong" },
token
);
}, compare);
}, threw(403));
});
});

Expand Down Expand Up @@ -1520,6 +1549,7 @@ describe("WhatsAppAPI", function () {
billable: true,
category: "business-initiated"
};
const biz_opaque_callback_data = "5";

const valid_message_mock = new MessageWebhookMock(
phoneID,
Expand All @@ -1533,7 +1563,8 @@ describe("WhatsAppAPI", function () {
status,
id,
conversation,
pricing
pricing,
biz_opaque_callback_data
);

const Whatsapp = new WhatsAppAPI({
Expand All @@ -1553,47 +1584,39 @@ describe("WhatsAppAPI", function () {
describe("Validation", function () {
describe("Secure truthy (default)", function () {
it("should throw 400 if rawBody is missing", function () {
const compare = (e) => e === 400;

rejects(Whatsapp.post(valid_message_mock), compare);
rejects(Whatsapp.post(valid_message_mock), threw(400));

rejects(
Whatsapp.post(valid_message_mock, undefined),
compare
threw(400)
);
});

it("should throw 401 if signature is missing", function () {
const compare = (e) => e === 401;

rejects(
Whatsapp.post(valid_message_mock, body),
compare
threw(401)
);

rejects(
Whatsapp.post(valid_message_mock, body, undefined),
compare
threw(401)
);
});

it("should throw 500 if appSecret is not specified", function () {
const compare = (e) => e === 500;

delete Whatsapp.appSecret;

rejects(
Whatsapp.post(valid_message_mock, body, signature),
compare
threw(500)
);
});

it("should throw 401 if the signature doesn't match the hash", function () {
const compare = (e) => e === 401;

rejects(
Whatsapp.post(valid_message_mock, body, "wrong"),
compare
threw(401)
);
});

Expand Down Expand Up @@ -1626,11 +1649,9 @@ describe("WhatsAppAPI", function () {
});

it("should throw 400 if the request isn't a valid WhatsApp Cloud API request (data.object)", function () {
const compare = (e) => e === 400;

Whatsapp.secure = false;

rejects(Whatsapp.post({}), compare);
rejects(Whatsapp.post({}), threw(400));
});
});

Expand Down Expand Up @@ -1776,6 +1797,7 @@ describe("WhatsAppAPI", function () {
id,
conversation,
pricing,
biz_opaque_callback_data,
raw: valid_status_mock
});
});
Expand Down
15 changes: 14 additions & 1 deletion test/webhooks.mocks.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ class StatusWebhookMock {
/**
* Helper class to test the status post request, conditionally creating the object based on the available data
*/
constructor(phoneID, phone, status, messageID, conversation, pricing) {
constructor(
phoneID,
phone,
status,
messageID,
conversation,
pricing,
biz_opaque_callback_data
) {
this.object = "whatsapp_business_account";
this.entry = [
{
Expand Down Expand Up @@ -95,6 +103,11 @@ class StatusWebhookMock {
this.entry[0].changes[0].value.statuses[0].pricing = pricing;
}

if (biz_opaque_callback_data) {
this.entry[0].changes[0].value.statuses[0].biz_opaque_callback_data =
biz_opaque_callback_data;
}

if (
Object.keys(this.entry[0].changes[0].value.statuses[0]).length === 0
) {
Expand Down

0 comments on commit 9c0882b

Please sign in to comment.