From adf4e72f751cc22943dd1d502b260fdf5a6365e0 Mon Sep 17 00:00:00 2001 From: Samarth Kulkarni <10831629+ParadoxInfinite@users.noreply.github.com> Date: Thu, 18 Nov 2021 01:45:54 +0530 Subject: [PATCH] feat: initial draft of nodetifications need to add more providers and modes of notifications --- README.md | 25 ++++++++++++ index.js | 77 +++++++++++++++++++++++++++++++++++++ package-lock.json | 98 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 34 ++++++++++++++++ types/index.d.ts | 32 ++++++++++++++++ utils/errors.js | 22 +++++++++++ 6 files changed, 288 insertions(+) create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 types/index.d.ts create mode 100644 utils/errors.js diff --git a/README.md b/README.md index dbae484..134b429 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ # nodetifications + Notifications made easy in NodeJS! + +## Features +- Very (c)lean package! < 4kB zipped and < 8kB unzipped, and documented with JSDoc annotations. +- Multiple providers and notification modes in 1 package. +- Very lean dependency, which means less points of failure. +- Highly flexible to your needs, change provider/creds anywhere in between. +- Standardized response formats. All successful requests will return JSON responses. +- Promise based for asynchronous support and no [callback hell](http://callbackhell.com/). + +## Installation + +```zsh +npm install nodetifications +``` + +## Usage + +Complete usage docs with sample code - COMING SOON. + +## Contributing +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +## License +[MIT](https://choosealicense.com/licenses/mit/) \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..a5104f8 --- /dev/null +++ b/index.js @@ -0,0 +1,77 @@ +import fetch, { Headers } from 'node-fetch'; + +import { GenericError } from './utils/errors.js'; + +const errorCheck = (from, to, userId, password) => { + if (!userId || !password) { + throw new GenericError({ + errType: 'INVALID_CREDENTIALS', + errMsg: 'Invalid userId and password provided, check the credentials again' + }) + } + if (!from) throw new GenericError({ + errType: 'INVALID_FROM', + errMsg: 'Invalid From Number' + }) + if (!to) throw new GenericError({ + errType: 'INVALID_TO', + errMsg: 'Invalid To Number' + }) +} + +const gupShupSMSBaseURL = 'https://enterprise.smsgupshup.com/GatewayAPI/rest'; +const twilioSMSBaseURL = (userId) => `https://api.twilio.com/2010-04-01/Accounts/${userId}/Messages.json`; + +export class SMS { + constructor({ userId, password, provider, from } = {}) { + this.userId = userId; + this.password = password; + this.provider = provider; + this.from = from; + } + + sendSMS({ + provider = this.provider, + from = this.from, + to, + message = [], + userId = this.userId, + password = this.password + } = {}) { + if (provider !== this.provider && (userId === this.userId || password === this.password)) { + throw new GenericError({ + errType: 'INVALID_CREDENTIALS', + errMsg: 'Provider has changed but the credentials have not, if you\'re using the same creds in multiple places, please stop. Get some help.' + }) + } + let encodedMessage; + errorCheck(from, to, userId, password); + if (typeof message !== 'string') { + if (message[0]) { + encodedMessage = message.reduce( + (interimMessage, line, index) => `${interimMessage}${encodeURIComponent(line)}${index === message.length - 1 ? '' : '%0A'}` + , ''); + } else { + encodedMessage = '' + } + } else { + message = encodeURIComponent(message); + } + switch (provider) { + case 'gupshup': return fetch(`${gupShupSMSBaseURL}?method=SendMessage&send_to=${to}&msg=${encodedMessage}&msg_type=TEXT&userid=${userId}&auth_scheme=plain&password=${password}&v=1.1&format=json`).then(result => result.json()); + case 'twilio': const headers = new Headers(); + const auth = Buffer.from(`${userId}:${password}`); + headers.append('Authorization', 'Basic ' + auth.toString('base64')); + return fetch(twilioSMSBaseURL(userId), { method: 'POST', headers, body: { Body: encodedMessage, From: encodeURIComponent(from), To: encodeURIComponent(to) } }).then(response => response.json()); + default: throw new GenericError({ + errType: 'PROVIDER_ERR', + errMsg: 'Invalid provider', + additionalDetails: 'If you would like to see this provider integrated, please raise an issue on https://github.com/ParadoxInfinite/nodetifications/issues/.' + }); + } + } +} + +export default { + SMS +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..de1de77 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,98 @@ +{ + "name": "nodetifications", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "nodetifications", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "node-fetch": "^3.0.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/fetch-blob": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz", + "integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/node-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", + "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", + "dependencies": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz", + "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==", + "engines": { + "node": ">= 8" + } + } + }, + "dependencies": { + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + }, + "fetch-blob": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.3.tgz", + "integrity": "sha512-ax1Y5I9w+9+JiM+wdHkhBoxew+zG4AJ2SvAD1v1szpddUIiPERVGBxrMcB2ZqW0Y3PP8bOWYv2zqQq1Jp2kqUQ==", + "requires": { + "web-streams-polyfill": "^3.0.3" + } + }, + "node-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", + "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", + "requires": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^3.1.2" + } + }, + "web-streams-polyfill": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz", + "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..837f01f --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "nodetifications", + "version": "0.0.1", + "description": "Notifications made easy in NodeJS", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/ParadoxInfinite/nodetifications.git" + }, + "keywords": [ + "notifications", + "node", + "gupshup" + ], + "types": "./types/index.d.ts", + "files": [ + "index.js", + "types/index.d.ts", + "utils/errors.js" + ], + "author": "Samarth Kulkarni", + "license": "MIT", + "bugs": { + "url": "https://github.com/ParadoxInfinite/nodetifications/issues" + }, + "homepage": "https://github.com/ParadoxInfinite/nodetifications#readme", + "dependencies": { + "node-fetch": "^3.0.0" + } +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..0ccf8f5 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,32 @@ +export class SMS { + /** + * @param {string} userId - User ID to authenticate with the provider + * @param {string} password - Password to authenticate with the provider + * @param {string} provider - Provider to send messages from. Currently valid providers: `twilio` and `gupshup` + * @param {string} from - Number from which to send the SMS, can be changed per request too, @see `SMS.sendSMS` + */ + constructor({ userId, password, provider, from }: { + userId: string, + password: string, + provider: string, + from: string + }) + + /** + * @param {string} provider - Defaults to constructor value, but can give a different `provider` than before, however this makes `userId` and `password` mandatory. + * @param {string} from - Defaults to constructor value, but can be different than the `from` before. + * @param {string} to - Mandatory value, recipients's phone number. + * @param {Array} message - Array of strings, each element is a new line. Blank string(without whitespace) for a blank new line. + * @param {string} userId - Defaults to constructor value, but mandatory when `provider` is passed and different from constructor. + * @param {string} password - Defaults to constructor value, but mandatory when `provider` is passed and different from constructor. + * @returns {Promise} A response promise from the provider, if successful, the response will be in JSON format. Errors need to be handled separately. + */ + sendSMS({ provider, from, to, message, userId, password }: { + provider: string, + from: string, + to: string, + message: Array, + userId: string, + password: string + }): Promise +} \ No newline at end of file diff --git a/utils/errors.js b/utils/errors.js new file mode 100644 index 0000000..23b281f --- /dev/null +++ b/utils/errors.js @@ -0,0 +1,22 @@ +const Reset = "\x1b[0m" +const Bright = "\x1b[1m" + +const FgWhite = "\x1b[37m" + +const BgRed = "\x1b[41m" + +export class GenericError extends Error { + constructor({ + errType = 'UNKNOWN_ERROR', + errMsg = 'Unknown error occured', + additionalDetails = "If you would like to report this to the author, please raise an issue here: https://github.com/ParadoxInfinite/nodetifications/issues/" + }) { + super(); + this.name = `${Bright}${BgRed}${FgWhite}${errType}`, + this.message = `${errMsg}.${Reset}\n${additionalDetails}` + } +} + +export default { + GenericError +} \ No newline at end of file