UNPKG

wh-wrapper

Version:

JavaScript wrapper for the WhatsApp Cloud API

789 lines (788 loc) 36.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const express_1 = __importDefault(require("express")); const axios_1 = __importStar(require("axios")); const fs_1 = __importDefault(require("fs")); const form_data_1 = __importDefault(require("form-data")); const message_handler_1 = __importDefault(require("../handlers/message-handler")); const schema_1 = require("../schemas/schema"); const callback_handler_1 = __importDefault(require("../handlers/callback-handler")); const update_handler_1 = __importDefault(require("../handlers/update-handler")); const request_welcome_handler_1 = __importDefault(require("../handlers/request-welcome-handler")); const error_classes_1 = require("../errors/error-classes"); const flow_handler_1 = __importDefault(require("../handlers/flow-handler")); class Client extends events_1.EventEmitter { constructor(phoneID, token, verifyToken, options = {}) { var _a, _b, _c, _d; super(); this.phoneID = phoneID; this.token = token; this.verifyToken = verifyToken; this.options = options; this._untypedOn = this.on; this.on = (event, listener) => this._untypedOn(event, listener); this.commonKeys = { messaging_product: "whatsapp", recipient_type: "individual" }; console.clear(); if (!this.phoneID || !this.token) throw new Error("Missing Parameters, phoneID and token are required"); this.url = `${((_a = this.options) === null || _a === void 0 ? void 0 : _a.baseURL) || "https://graph.facebook.com"}/v${((_c = (_b = this.options) === null || _b === void 0 ? void 0 : _b.apiVersion) === null || _c === void 0 ? void 0 : _c.toString()) || "22.0"}`; this.axiosInstance = axios_1.default.create({ baseURL: this.url, headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, }, }); if ((_d = this.options) === null || _d === void 0 ? void 0 : _d.callbackUrl) { if (!this.options.appID || !this.options.appSecret) throw new Error("Missing Parameters, appId and appSecret are required"); this._setCallBackUrl(this.options.callbackUrl, this.options.appID.toString(), this.options.appSecret); } if (this.verifyToken) this._initialize(); } async _setCallBackUrl(callbackUrl, appID, appSecret) { const getAppAcsessToken = await this.makeRequest({ method: "GET", url: `/oauth/access_token`, params: { grant_type: "client_credentials", client_id: appID, client_secret: appSecret, }, }); const response = await this.makeRequest({ method: "POST", url: `/${appID}/subscriptions`, params: { object: "whatsapp_business_account", callback_url: callbackUrl + this.options.webHookEndpoint || "", verify_token: this.verifyToken, access_token: getAppAcsessToken.access_token, fields: ["message_template_status_update", "messages"].join(","), }, }); if (response.success) console.info("CallBack URL successfully registered!"); return response; } async _initialize() { var _a, _b; if (!this.options.server || !this.options.server.hasOwnProperty("listen") || typeof this.options.server.listen !== "function") { console.warn("A proper Express instance was not provided, an Express server operator..."); const port = this.options.port || 3000; this.options.server = (0, express_1.default)(); this.options.server.listen(port); this.options.server.use(express_1.default.json()); console.warn(`Express server listening on http://localhost:${port}, port: ${port}`); } if (!this.verifyToken) throw new Error("A proper verify token must be provided."); this.options.server.get(((_a = this === null || this === void 0 ? void 0 : this.options) === null || _a === void 0 ? void 0 : _a.webHookEndpoint) || "/", (req, res) => { return req.query["hub.verify_token"] === this.verifyToken ? res.status(200).send(req.query["hub.challenge"]) : res.status(403).send("Error, invalid verification token"); }); this.options.server.post(((_b = this.options) === null || _b === void 0 ? void 0 : _b.webHookEndpoint) || "/", (req, res) => { try { const field = req.body.entry[0].changes[0].field; const value = req.body.entry[0].changes[0].value; if (value.metadata.phone_number_id !== this.phoneID) return res.status(200).send(); if (field === "messages") { if ("messages" in value) { if (value.messages[0].type === "interactive") { if (value.messages[0].interactive.type === "nfm_reply") this.emit("flowCompletion", new flow_handler_1.default(this, value)); else this.emit("callbacks", new callback_handler_1.default(this, value)); } else if ([ "text", "image", "sticker", "video", "document", "audio", "location", "contacts", "unsupported", ].includes(value.messages[0].type)) { this.emit("messages", new message_handler_1.default(this, value)); } else if (value.messages[0].type === "request_welcome") { this.emit("chatOpened", new request_welcome_handler_1.default(this, value)); } else return; } else if ("statuses" in value) { this.emit("statuses", new update_handler_1.default(this, value)); } } return res.status(200).send(); } catch (e) { res.status(500).send(); } }); } async makeRequest(config) { var _a, _b; try { const response = await this.axiosInstance({ ...config, }); return response.data; } catch (e) { if ((0, axios_1.isAxiosError)(e)) { const response = e.response; const code = response.data.error.code; const message = response.data.error.message; const fbtrace_id = (_a = response.data.error) === null || _a === void 0 ? void 0 : _a.fbtrace_id; const type = response.data.error.type; const details = (_b = response.data.error.error_data) === null || _b === void 0 ? void 0 : _b.details; switch (true) { case [0, 3, 10, 190, 200].includes(code): throw new error_classes_1.AuthError(code, message, fbtrace_id, type, details); case [4, 80007, 130429, 131048, 131056, 133016].includes(code): throw new error_classes_1.ThrottlingError(code, message, fbtrace_id, type, details); case [368, 131031].includes(code): throw new error_classes_1.IntegrityError(code, message, fbtrace_id, type, details); case [100, 131008, 131009].includes(code): throw new error_classes_1.ParameterError(code, message, fbtrace_id, type, details); case [131021, 131026, 131047, 131051, 131052, 131053].includes(code): throw new error_classes_1.MessageError(code, message, fbtrace_id, type, details); case [132000, 132001, 132005, 132007, 132012, 132015, 132016, 132068, 132069].includes(code): throw new error_classes_1.TemplateError(code, message, fbtrace_id, type, details); case [133000, 133004, 133005, 133006, 133008, 133009, 133010, 133015].includes(code): throw new error_classes_1.RegistrationError(code, message, fbtrace_id, type, details); case [131042].includes(code): throw new error_classes_1.BillingError(code, message, fbtrace_id, type, details); default: throw new error_classes_1.UnknownError(code, message, fbtrace_id, type, details); } } else { throw new Error("unknown error"); } } } async createTemplate(template) { var _a, _b, _c, _d; if (!this.options.businessAccountID) throw new Error("You must provide a business account Id before used"); const validation = schema_1.CreateTempleteSchema.safeParse(template); if (!validation.success) { throw new error_classes_1.ParametersError("Error in the parameters you provided", validation.error); } const data = { name: template.name.replace(/ /g, "_"), language: schema_1.Language[template.language], category: template.category, allow_category_change: true, components: [], }; if (template.header) { const header = { type: "HEADER" }; header.format = template.header.type; if (template.header.type === "IMAGE" || template.header.type === "VIDEO" || template.header.type === "DOCUMENT") { header.example = { header_handle: [template.header.media] }; } else if (template.header.type === "TEXT" && template.header.text) { const variables = template.header.text.match(/\{\{(.*?)\}\}/g); if (variables && (variables === null || variables === void 0 ? void 0 : variables.length) > 0) { template.header.text = template.header.text.split(variables[0].trim()).join("{{1}}"); } header.text = template.header.text; if (variables) header.example = { header_text: variables === null || variables === void 0 ? void 0 : variables.map((v) => v.slice(2, -2)), }; } (_a = data.components) === null || _a === void 0 ? void 0 : _a.push(header); } const body = { type: "BODY" }; if (typeof template.body === "string") { const variables = template.body.match(/\{\{(.*?)\}\}/g); if (variables && variables.length > 0) { variables.map((v, i) => (template.body = template.body.split(v.trim()).join(`{{${i + 1}}}`))); } body.text = template.body; if (variables) body.example = { body_text: [(variables === null || variables === void 0 ? void 0 : variables.map((v) => v.slice(2, -2))) || []], }; } else { body.add_security_recommendation = template.body.addSecurityRecommendation; } (_b = data.components) === null || _b === void 0 ? void 0 : _b.push(body); if (template.footer) (_c = data.components) === null || _c === void 0 ? void 0 : _c.push({ type: "FOOTER", text: template.footer, }); if (template.buttons) { const buttonsData = { type: "BUTTONS", buttons: [] }; if (!Array.isArray(template.buttons)) template.buttons = [template.buttons]; if (template.buttons.filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "UrlButton").length > 2) throw new Error("Max 2 URL buttons allowed"); if (template.buttons.filter((b) => (b === null || b === void 0 ? void 0 : b.type) === "PhoneNumberButton").length > 1) throw new Error("Max 1 phone number button allowed"); template.buttons.map((button) => { switch (button === null || button === void 0 ? void 0 : button.type) { case "UrlButton": const variables = button.url.match(/\{\{(.*?)\}\}/g); variables && variables.map((v, i) => (button.url = button.url.split(v.trim()).join(`{{${i + 1}}}`))); buttonsData.buttons.push({ type: "URL", text: button.title, url: button.url, ...(variables && variables.length > 0 ? { example: variables === null || variables === void 0 ? void 0 : variables.map((v) => `${v.slice(2, -2)}`), } : { example: [button.url] }), }); break; case "PhoneNumberButton": buttonsData.buttons.push({ type: "PHONE_NUMBER", text: button.title, phone_number: button.phoneNumber, }); break; case "QuickReplyButton": buttonsData.buttons.push({ type: "QUICK_REPLY", text: button.text, }); break; case "OTPButton": const otp = { type: "OTP", otp_type: button.OTPType, }; if (button.packageName) otp.package_name = button.packageName; if (button.signatureHash) otp.signature_hash = button.signatureHash; buttonsData.buttons.push(otp); break; case "CopyCodeButton": buttonsData.buttons.push({ type: "QUICK_REPLY", example: button.example, }); break; default: break; } }); buttonsData.buttons.sort((a, b) => { const typeOrder = { QUICK_REPLY: 1, URL: 2, PHONE_NUMBER: 3, COPY_CODE: 4, }; return typeOrder[a.type] - typeOrder[b.type]; }); (_d = data.components) === null || _d === void 0 ? void 0 : _d.push(buttonsData); } console.log(JSON.stringify(data)); const response = await this.makeRequest({ method: "POST", url: `/${this.options.businessAccountID}/message_templates`, data, }); return { ...response, templateName: template.name.replace(/ /g, "_"), }; } async sendTemplate(to, template, options) { const validation = schema_1.SendTemplateSchema.safeParse(template); if (!validation.success) { throw new error_classes_1.ParametersError("Error in the parameters you provided", validation.error); } if (!Array.isArray(template.body)) template.body = [template.body]; if (!Array.isArray(template === null || template === void 0 ? void 0 : template.buttons)) template.buttons = [template.buttons]; const templateObjsct = { to, type: "template", ...((options === null || options === void 0 ? void 0 : options.messageID) && { context: { message_id: options === null || options === void 0 ? void 0 : options.messageID }, }), template: { name: template.name, language: { code: schema_1.Language[template.language], }, components: [ ...(template.header ? [ { type: "header", parameters: [ { ...(typeof template.header === "string" ? { type: "text", value: template.header, } : { parameters: [ { type: template.header.type.toLowerCase(), [template.header.type.toLowerCase()]: {}, }, ], }), }, ], }, ] : []), { type: "body", parameters: template.body.map((c) => typeof c === "string" ? { type: "text", text: c } : "currency" in c ? { type: "currency", currency: { fallback_value: c.currency, code: c.code, amount_1000: c.amount, }, } : "dateTime" in c && { type: "date_time", date_time: { fallback_value: c.dateTime }, }), }, ...(template.buttons ? template.buttons.flatMap((p, i) => { switch (p === null || p === void 0 ? void 0 : p.type) { case "QuickReplyButton": return { type: "button", sub_type: "quick_reply", index: i, parameters: [{ type: "payload", payload: p.text }], }; case "UrlButton": return { type: "button", sub_type: "url", index: i, text: p.title, url: p.url, }; case "PhoneNumberButton": return { type: "button", sub_type: "phone_number", index: i, text: p.title, phone_number: p.phoneNumber, }; default: return []; } }) : []), ].filter(Boolean), }, }; return (await this._sendMessage(templateObjsct)).messages[0].id; } async _sendMessage(content) { return await this.makeRequest({ method: "POST", url: `/${this.phoneID}/messages`, data: { ...this.commonKeys, ...content, }, }); } async sendInteractiveMessage(to, text, data, header) { let buttonArray; let buttonObject = { buttons: [], }; let buttonTypes = ""; if (data.buttons && !Array.isArray(data.buttons)) buttonArray = [data.buttons]; else buttonArray = data.buttons; buttonArray === null || buttonArray === void 0 ? void 0 : buttonArray.map((button) => { if ("sections" in button) { buttonTypes = "list"; buttonObject = { button: button.buttonTitle, sections: button.sections.map((section) => ({ title: section.title, rows: section.rows.map((row) => ({ title: row.title, id: row.callbackData, description: row.description, })), })), }; } else if ("url" in button) { buttonTypes = "cta_url"; buttonObject = { name: "cta_url", parameters: { display_text: button.title, url: button.url }, }; } else if ("callbackData" in button) { buttonTypes = "button"; buttonObject = { buttons: [ ...buttonObject.buttons, { type: "reply", reply: { title: button.title, id: button.callbackData }, }, ], }; } else if ("catalog_id" in button) { } else throw new Error(""); }); return (await this._sendMessage({ to, ...(data.messageID && { context: { message_id: data.messageID }, }), type: "interactive", interactive: { type: buttonTypes, body: { text }, ...(data.header && { header: { type: "text", text: data.header, }, }), ...((header === null || header === void 0 ? void 0 : header.content) && { header: { type: header === null || header === void 0 ? void 0 : header.type, ...header.content, }, }), ...(data.footer && { footer: { text: data.footer, }, }), action: buttonObject, }, })).messages[0].id; } async sendMedia(type, to, media, options) { const validation = await schema_1.sendMediaInteractiveSchema.safeParse(options); if (!validation.success) { throw new error_classes_1.ParametersError("Error in the parameters you provided", validation.error); } if (!(options === null || options === void 0 ? void 0 : options.buttons)) { return (await this._sendMessage({ to, type: type.toLowerCase(), ...((options === null || options === void 0 ? void 0 : options.messageID) && { context: { message_id: options.messageID }, }), [type.toLowerCase()]: { ...((options === null || options === void 0 ? void 0 : options.capiton) && { caption: options.capiton }), ...(media.startsWith("http") && { link: media }), ...(isNaN(Number(media)) && !media.startsWith("http") && { id: await this.uploadMedia(media), }), ...(Number(media) && { id: media }), ...(type === "DOCUMENT" && (options === null || options === void 0 ? void 0 : options.filename) && { filename: options === null || options === void 0 ? void 0 : options.filename }), }, })).messages[0].id; } else { if (!options.capiton) throw new Error("לא ניתן לספק כפתורים ללא תיאור"); return await this.sendInteractiveMessage(to, options.capiton, { ...(options.messageID && { messageId: options.messageID }), ...(options.footer && { footer: options.footer }), buttons: options.buttons, }, { type: type.toLowerCase(), content: { [type.toLowerCase()]: { ...(media.startsWith("http") && { link: media }), ...(isNaN(Number(media)) && !media.startsWith("http") && { id: await this.uploadMedia(media), }), ...(Number(media) && { id: media }), ...(type === "DOCUMENT" && (options === null || options === void 0 ? void 0 : options.filename) && { filename: options === null || options === void 0 ? void 0 : options.filename }), }, }, }); } } async sendMessage(to, text, options) { const validation = schema_1.sendMessageSchema.safeParse({ to, text, options }); if (!validation.success) { throw new error_classes_1.ParametersError("Error in the parameters you provided", validation.error); } const data = { to, }; if (options === null || options === void 0 ? void 0 : options.messageID) data.context = { message_id: options.messageID }; if (!(options === null || options === void 0 ? void 0 : options.buttons) && !(options === null || options === void 0 ? void 0 : options.header) && !(options === null || options === void 0 ? void 0 : options.footer)) { return (await this._sendMessage({ ...data, type: "text", text: { body: text, preview_url: options === null || options === void 0 ? void 0 : options.previewUrl }, })).messages[0].id; } else return await this.sendInteractiveMessage(to, text, options); } async sendReaction(to, emoji, messageID) { const data = { to, type: "reaction", reaction: { message_id: messageID, emoji, }, }; return (await this._sendMessage(data)).messages[0].id; } async removeReaction(to, messageID) { return await this.sendReaction(to, messageID, ""); } async sendLocation(to, location, options) { if (!location.latitude || !location.longitude) throw new Error("location is must"); const data = { to, ...((options === null || options === void 0 ? void 0 : options.messageID) && { context: { message_id: options.messageID } }), type: "location", location: { ...location, ...options, }, }; return (await this._sendMessage(data)).messages[0].id; } async sendSticker(to, sticker, options) { return await this.sendMedia("STICKER", to, sticker, options); } async sendVideo(to, video, options) { return await this.sendMedia("VIDEO", to, video, options); } async sendDocument(to, document, options) { return await this.sendMedia("DOCUMENT", to, document, options); } async sendAudio(to, audio, options) { return await this.sendMedia("AUDIO", to, audio, options); } async sendImage(to, image, options) { return await this.sendMedia("IMAGE", to, image, options); } async sendContact(to, name, phone, options) { const validation = await schema_1.SendContacSchema.safeParse({ to, name, phones: phone, options, }); if (!validation.success) { throw new error_classes_1.ParametersError("Error in the parameters you provided", validation.error); } return (await this._sendMessage({ to, ...((options === null || options === void 0 ? void 0 : options.messageID) && { context: { message_id: options.messageID }, }), type: "contacts", contacts: [ { name: typeof name === "string" ? { first_name: name, formatted_name: name } : name, phones: typeof phone === "string" ? { phone: "+" + phone } : !Array.isArray(phone) ? [phone] : phone, ...((options === null || options === void 0 ? void 0 : options.birthday) && { birthday: options.birthday }), ...((options === null || options === void 0 ? void 0 : options.Org) && { org: options.Org }), ...((options === null || options === void 0 ? void 0 : options.emails) && { emails: !Array.isArray(options.emails) ? [options.emails] : options.emails, }), ...((options === null || options === void 0 ? void 0 : options.addresses) && { addresses: !Array.isArray(options.addresses) ? [options.addresses] : options.addresses, }), ...((options === null || options === void 0 ? void 0 : options.urls) && { urls: !Array.isArray(options.urls) ? [options.urls] : options.urls, }), }, ], })).messages[0].id; } async sendRowRequest(obj) { return await this.makeRequest({ ...obj, url: obj.endpoint }); } async markMessageAsRead(messageID) { const data = { messaging_product: "whatsapp", status: "read", message_id: messageID, }; return (await this._sendMessage(data)).success; } async deleteMedia(mediaID) { return (await this.makeRequest({ method: "DELETE", url: `/${mediaID.toString()}`, params: { phone_number_id: this.phoneID }, })).success; } async uploadMedia(media) { const data = new form_data_1.default(); data.append("messaging_product", "whatsapp"); if (media.startsWith("http")) { const response = await axios_1.default.get(media, { responseType: "arraybuffer" }); const mimeType = response.headers["content-type"]; data.append("file", response.data, { contentType: mimeType, filename: media.split("/").pop(), }); data.append("type", mimeType); } else { if (!fs_1.default.existsSync(media)) throw new Error("חובה לספק נתיב קובץ תקין"); data.append("file", fs_1.default.createReadStream(media)); } return (await this.makeRequest({ method: "POST", url: `/${this.phoneID}/media`, data, headers: { ...data.getHeaders(), }, })).id; } async getProfile() { const phoneData = await this.makeRequest({ method: "GET", url: `/${this.phoneID}/`, }); const fields = ["about", "address", "description", "email", "profile_picture_url", "websites", "vertical"]; const profile = (await this.makeRequest({ method: "GET", url: `/${this.phoneID}/whatsapp_business_profile?fields=${fields.join(",")}`, })).data[0]; return { verifiedName: phoneData.verified_name, phoneNumber: phoneData.display_phone_number, phoneNumberID: phoneData.id, qualityRating: phoneData.quality_rating, about: profile.about, description: profile.description, address: profile.address, email: profile.email, profilePictureUrl: profile.profile_picture_url, websites: profile.websites, vertical: profile.vertical, }; } async registerPhoneNumber(pin, dataLocalizationRegion) { return await this.makeRequest({ url: `/${this.phoneID}/register`, method: "POST", data: { ...this.commonKeys, pin, ...(dataLocalizationRegion ? { dataLocalizationRegion } : {}), }, }); } async setBusinessPublicKey(publicKey) { return (await this.makeRequest({ url: `/${this.phoneID}/whatsapp_business_encryption`, method: "POST", data: { business_public_key: publicKey }, })).success; } async updateBusinessProfile(info) { const validation = schema_1.UpdateBusinessProfileSchema.safeParse(info); if (!validation.success) { throw new error_classes_1.ParametersError("Error in the parameters you provided", validation.error); } const data = Object.fromEntries(Object.entries({ about: info.about, address: info.address, description: info.description, email: info.email, profile_picture_handle: info.profilePictureHandle, vertical: info.industry.toString().toLowerCase(), websites: info.websites, }).filter(([_, v]) => v != null && v !== "")); return (await this.makeRequest({ url: `/${this.phoneID}/whatsapp_business_profile`, method: "POST", data, })); } } exports.default = Client;