diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e8203db..c681144 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,33 +1,37 @@ -# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created -# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages - -name: Node.js Package +name: Publish to NPM on: - release: - types: [created] + push: + branches: + - main jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 - - run: npm ci - - run: npm test + test: + runs-on: ubuntu-latest + environment: Staging Environment + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + - run: npm i + - run: npm test + env: + accessToken: ${{secrets.accessToken}} + graphAPIVersion: ${{secrets.graphAPIVersion}} + senderPhoneNumberId: ${{secrets.senderPhoneNumberId}} + WABA_ID: ${{secrets.WABA_ID}} - publish-npm: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 16 - registry-url: https://registry.npmjs.org/ - - run: npm ci - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + publish-npm: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm i + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN_IN_GITHUB_ACTIONS}} \ No newline at end of file diff --git a/index.js b/index.js index 217cc9d..31b8a5f 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,19 @@ const unirest = require('unirest'); const signale = require('signale'); const fs = require('fs'); +const request = require('request'); const messageParser = require('./msg_parser.js'); +const { Readable } = require('stream'); + +// check if file exists +const fileExists = ({ path }) => { + try { + fs.accessSync(path, fs.constants.F_OK); + return true; + } catch (err) { + return false; + } +}; class WhatsappCloud { constructor({ @@ -146,33 +158,6 @@ class WhatsappCloud { } }; - this._uploadMedia = async ({ file_path, file_name }) => { - return new Promise((resolve, reject) => { - const mediaFile = fs.createReadStream(file_path); - // type = type || 'image'; - unirest( - 'POST', - `https://graph.facebook.com/${this.graphAPIVersion}/${this.senderPhoneNumberId}/media` - ) - .headers({ - Authorization: `Bearer ${this.accessToken}`, - }) - .field('messaging_product', 'whatsapp') - .attach('file', mediaFile) - .end((res) => { - if (res.error) { - reject(res.error); - } else { - let response = JSON.parse(res.raw_body); - resolve({ - status: 'success', - media_id: response.id, - file_name: file_name || null, - }); - } - }); - }); - }; this._retrieveMediaUrl = async ({ media_id }) => { const response = await this._fetchAssistant({ baseUrl: `https://graph.facebook.com/${this.graphAPIVersion}`, @@ -512,17 +497,42 @@ class WhatsappCloud { return response; } - async sendImage({ recipientPhone, caption, file_path, file_name, url }) { + async sendImage({ + recipientPhone, + caption, + file_path, + file_name, + url, + media_id, + mime_type, + }) { this._mustHaverecipientPhone(recipientPhone); - if (file_path && url) { + const hasFileAndUrl = file_path && url; + const hasMediaIdAndFile = media_id && file_path; + const hasMediaIdAndUrl = media_id && url; + const lacksAll = !file_path && !url && !media_id; + + if (hasFileAndUrl) { throw new Error( 'You can only send an image in your "file_path" or an image in a publicly available "url". Provide either "file_path" or "url".' ); } - if (!file_path && !url) { + if (hasMediaIdAndFile) { + throw new Error( + 'You can only send an image using a media_id or a file_path. Provide either "media_id" or "file_path".' + ); + } + + if (hasMediaIdAndUrl) { throw new Error( - 'You must send an image in your "file_path" or an image in a publicly available "url". Provide either "file_path" or "url".' + 'You can only send an image using a media_id or a url. Provide either "media_id" or "url".' + ); + } + + if (lacksAll) { + throw new Error( + 'You must have either an image in your "file_path" or an image in a publicly available "url", or a previously uploaded "media_id". Provide either "file_path" or "url" or "media_id".' ); } @@ -536,12 +546,15 @@ class WhatsappCloud { }, }; - if (file_path) { - let uploadedFile = await this._uploadMedia({ + if (media_id) { + body['image']['id'] = media_id; + } else if (file_path) { + let uploadedFile = await this.preUploadMedia({ file_path, file_name, + mime_type, }); - body['image']['id'] = uploadedFile.media_id; + body['image']['id'] = Number(uploadedFile.media_id); } else { body['image']['link'] = url; } @@ -557,7 +570,15 @@ class WhatsappCloud { body, }; } - async sendVideo({ recipientPhone, caption, file_path, file_name, url }) { + + async sendVideo({ + recipientPhone, + caption, + file_path, + file_name, + url, + mime_type, + }) { this._mustHaverecipientPhone(recipientPhone); if (file_path && url) { throw new Error( @@ -581,9 +602,10 @@ class WhatsappCloud { }, }; if (file_path) { - let uploadedFile = await this._uploadMedia({ + let uploadedFile = await this.preUploadMedia({ file_path, file_name, + mime_type, }); body['video']['id'] = uploadedFile.media_id; } else { @@ -602,7 +624,14 @@ class WhatsappCloud { }; } - async sendAudio({ recipientPhone, caption, file_path, file_name, url }) { + async sendAudio({ + recipientPhone, + caption, + file_path, + file_name, + url, + mime_type, + }) { this._mustHaverecipientPhone(recipientPhone); if (file_path && url) { throw new Error( @@ -624,9 +653,10 @@ class WhatsappCloud { audio: {}, }; if (file_path) { - let uploadedFile = await this._uploadMedia({ + let uploadedFile = await this.preUploadMedia({ file_path, file_name, + mime_type, }); body['audio']['id'] = uploadedFile.media_id; } else { @@ -645,7 +675,7 @@ class WhatsappCloud { }; } - async sendDocument({ recipientPhone, caption, file_path, url }) { + async sendDocument({ recipientPhone, caption, file_path, url, mime_type }) { this._mustHaverecipientPhone(recipientPhone); if (file_path && url) { throw new Error( @@ -674,9 +704,10 @@ class WhatsappCloud { }; if (file_path) { - let uploadedFile = await this._uploadMedia({ + let uploadedFile = await this.preUploadMedia({ file_path, file_name: caption, + mime_type, }); body['document']['id'] = uploadedFile.media_id; body['document']['filename'] = uploadedFile.file_name || ''; @@ -921,6 +952,87 @@ class WhatsappCloud { async getUserStatusPicture({ recipientPhone }) {} + async preUploadMedia({ file_path, file_name, file_buffer, mime_type }) { + return new Promise((resolve, reject) => { + let fileStream; + + if (!file_path && !file_buffer) { + return reject({ + status: 'failed', + error: 'You must provide either a file_path or a file_buffer.', + }); + } + + if (file_path) { + if (!fileExists({ path: file_path })) { + return reject({ + status: 'failed', + error: 'The file_path does not exist.', + }); + } else { + fileStream = fs.createReadStream(file_path); + } + } + + if (file_buffer) { + if (!Buffer.isBuffer(file_buffer)) { + return reject({ + status: 'failed', + error: 'The file_buffer is not a buffer.', + }); + } else { + // Convert buffer to a readable stream + fileStream = new Readable(); + fileStream.push(file_buffer); + fileStream.push(null); // Signal the end of the stream + } + } + + if (!mime_type) { + throw new Error('You must provide a "mime_type".'); + } + + const url = `https://graph.facebook.com/${this.graphAPIVersion}/${this.senderPhoneNumberId}/media`; + + const options = { + method: 'POST', + url, + headers: { + Authorization: `Bearer ${this.accessToken}`, + }, + formData: { + messaging_product: 'whatsapp', + type: mime_type, + file: { + value: fileStream, + options: { + filename: file_name, + contentType: null, + }, + }, + }, + }; + + request(options, function (error, response) { + if (error) { + reject({ + status: 'failed', + error, + }); + } else { + const data = JSON.parse(response.body); + const media_id = data.id; + + resolve({ + status: 'success', + media_id, + file_name: file_name || null, + }); + } + }); + }); + } + parseMessage(requestBody) { return messageParser({ requestBody, currentWABA_ID: this.WABA_ID }); } diff --git a/msg_parser.js b/msg_parser.js index 83fe2c4..cffac34 100644 --- a/msg_parser.js +++ b/msg_parser.js @@ -88,6 +88,8 @@ module.exports = ({ requestBody, currentWABA_ID }) => { msgType = 'ad_message'; } else if (message.type === 'text') { msgType = 'text_message'; + } else if (message.type === 'audio') { + msgType = 'audio_message'; } else if (message.type === 'sticker') { msgType = 'sticker_message'; } else if (message.type === 'image') { diff --git a/package.json b/package.json index 0bb66f5..e11d780 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,11 @@ "scripts": { "prettier": "prettier --ignore-path .prettierignore --config prettier.config.js --write .", "quickpush": "cls && git status && git add * && git commit * -m \"Updated Code\" && git push", - "test": "echo \"Error: no test specified\" && exit 0", + "test": "PACKAGE_VERSION=$npm_package_version echo \"Error: no test specified\" && exit 0", "npmpublish": "npm run prettier && np" }, "dependencies": { + "request": "^2.88.2", "signale": "^1.4.0", "unirest": "^0.6.0" },