├── .babelrc ├── .codeclimate.yml ├── .flowconfig ├── .github ├── dependabot.yml └── workflows │ ├── ci-test.yml │ ├── dependabot-auto-merge.yml │ └── publish-package.yml ├── .gitignore ├── .travis.yml ├── .yarnrc ├── LICENSE ├── README.md ├── __tests__ ├── custom-channel.js ├── notifme-sdk.js ├── providers │ ├── email │ │ ├── index.js │ │ ├── mailgun.js │ │ ├── mandrill.js │ │ ├── notificationCatcher.js │ │ ├── sendgrid.js │ │ ├── sendmail.js │ │ ├── ses.js │ │ ├── smtp.js │ │ └── sparkpost.js │ ├── mockHttp.js │ ├── slack │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── slack.js │ ├── sms │ │ ├── 46elks.js │ │ ├── callr.js │ │ ├── clickatell.js │ │ ├── index.js │ │ ├── infobip.js │ │ ├── nexmo.js │ │ ├── notificationCatcher.js │ │ ├── ovh.js │ │ ├── plivo.js │ │ ├── seven.js │ │ └── twilio.js │ ├── voice │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── twilio.js │ ├── webpush │ │ ├── gcm.js │ │ ├── index.js │ │ └── notificationCatcher.js │ └── whatsapp │ │ ├── index.js │ │ ├── infobip.js │ │ └── notificationCatcher.js ├── sender.js └── strategies │ └── providers │ ├── fallback.js │ ├── index.js │ ├── no-fallback.js │ └── roundrobin.js ├── docs ├── img │ ├── favicon.ico │ ├── getting-started-sms-catcher.png │ ├── getting-started-sms-log.png │ ├── icon.png │ └── logo.png └── index.html ├── examples ├── email.js ├── simple.js ├── slack.js └── with-notification-catcher.js ├── flow └── jest.js ├── lib ├── index.js ├── models │ ├── notification-request.js │ ├── provider-email.js │ ├── provider-push.js │ ├── provider-slack.js │ ├── provider-sms.js │ ├── provider-voice.js │ ├── provider-webpush.js │ └── provider-whatsapp.js ├── providers │ ├── email │ │ ├── index.js │ │ ├── mailgun.js │ │ ├── mandrill.js │ │ ├── notificationCatcher.js │ │ ├── sendgrid.js │ │ ├── sendmail.js │ │ ├── ses.js │ │ ├── smtp.js │ │ └── sparkpost.js │ ├── index.js │ ├── logger.js │ ├── notificationCatcherProvider.js │ ├── push │ │ ├── adm.js │ │ ├── apn.js │ │ ├── fcm.js │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── wns.js │ ├── slack │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── slack.js │ ├── sms │ │ ├── 46elks.js │ │ ├── callr.js │ │ ├── clickatell.js │ │ ├── index.js │ │ ├── infobip.js │ │ ├── nexmo.js │ │ ├── notificationCatcher.js │ │ ├── ovh.js │ │ ├── plivo.js │ │ ├── seven.js │ │ └── twilio.js │ ├── voice │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── twilio.js │ ├── webpush │ │ ├── gcm.js │ │ ├── index.js │ │ └── notificationCatcher.js │ └── whatsapp │ │ ├── index.js │ │ ├── infobip.js │ │ └── notificationCatcher.js ├── sender.js ├── strategies │ └── providers │ │ ├── fallback.js │ │ ├── index.js │ │ ├── no-fallback.js │ │ └── roundrobin.js └── util │ ├── aws │ ├── v4.js │ └── v4_credentials.js │ ├── crypto.js │ ├── dedupe.js │ ├── logger.js │ ├── registry.js │ └── request.js ├── package.json ├── src ├── index.js ├── models │ ├── notification-request.js │ ├── provider-email.js │ ├── provider-push.js │ ├── provider-slack.js │ ├── provider-sms.js │ ├── provider-voice.js │ ├── provider-webpush.js │ └── provider-whatsapp.js ├── providers │ ├── email │ │ ├── index.js │ │ ├── mailgun.js │ │ ├── mandrill.js │ │ ├── notificationCatcher.js │ │ ├── sendgrid.js │ │ ├── sendmail.js │ │ ├── ses.js │ │ ├── smtp.js │ │ └── sparkpost.js │ ├── index.js │ ├── logger.js │ ├── notificationCatcherProvider.js │ ├── push │ │ ├── adm.js │ │ ├── apn.js │ │ ├── fcm.js │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── wns.js │ ├── slack │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── slack.js │ ├── sms │ │ ├── 46elks.js │ │ ├── callr.js │ │ ├── clickatell.js │ │ ├── index.js │ │ ├── infobip.js │ │ ├── nexmo.js │ │ ├── notificationCatcher.js │ │ ├── ovh.js │ │ ├── plivo.js │ │ ├── seven.js │ │ └── twilio.js │ ├── voice │ │ ├── index.js │ │ ├── notificationCatcher.js │ │ └── twilio.js │ ├── webpush │ │ ├── gcm.js │ │ ├── index.js │ │ └── notificationCatcher.js │ └── whatsapp │ │ ├── index.js │ │ ├── infobip.js │ │ └── notificationCatcher.js ├── sender.js ├── strategies │ └── providers │ │ ├── fallback.js │ │ ├── index.js │ │ ├── no-fallback.js │ │ └── roundrobin.js └── util │ ├── aws │ ├── v4.js │ └── v4_credentials.js │ ├── crypto.js │ ├── dedupe.js │ ├── logger.js │ ├── registry.js │ └── request.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-flow" 5 | ], 6 | "plugins": [ 7 | [ 8 | "@babel/plugin-transform-runtime", 9 | { 10 | "corejs": 2 11 | } 12 | ], 13 | "@babel/plugin-transform-flow-strip-types", 14 | "@babel/plugin-syntax-dynamic-import", 15 | "@babel/plugin-syntax-import-meta", 16 | "@babel/plugin-proposal-class-properties", 17 | "@babel/plugin-proposal-json-strings", 18 | [ 19 | "@babel/plugin-proposal-decorators", 20 | { 21 | "legacy": true 22 | } 23 | ], 24 | "@babel/plugin-proposal-function-sent", 25 | "@babel/plugin-proposal-export-namespace-from", 26 | "@babel/plugin-proposal-numeric-separator", 27 | "@babel/plugin-proposal-throw-expressions" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: false 4 | config: 5 | plugins: 6 | - standard 7 | - babel-eslint 8 | - eslint-plugin-import 9 | duplication: 10 | enabled: true 11 | config: 12 | languages: 13 | - javascript: 14 | # nodesecurity: 15 | # enabled: true 16 | ratings: 17 | paths: 18 | - src/** 19 | exclude_paths: 20 | - "__tests__/" 21 | - "flow/" 22 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /lib/.* 3 | /node_modules/array.prototype.reduce/node_modules/es-abstract/tsconfig.tmp.json 4 | /node_modules/arraybuffer.prototype.slice/node_modules/es-abstract/tsconfig.tmp.json 5 | /node_modules/function.prototype.name/node_modules/es-abstract/tsconfig.tmp.json 6 | /node_modules/object.getownpropertydescriptors/node_modules/es-abstract/tsconfig.tmp.json 7 | /node_modules/string.prototype.trim/node_modules/es-abstract/tsconfig.tmp.json 8 | /node_modules/string.prototype.trimend/node_modules/es-abstract/tsconfig.tmp.json 9 | /node_modules/string.prototype.trimstart/node_modules/es-abstract/tsconfig.tmp.json 10 | 11 | [include] 12 | 13 | [libs] 14 | flow 15 | 16 | [options] 17 | suppress_comment= \\(.\\|\n\\)*\\$FlowIgnore 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | 10 | # Maintain dependencies for npm 11 | - package-ecosystem: "npm" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | commit-message: 16 | prefix: fix 17 | prefix-development: chore 18 | include: scope 19 | -------------------------------------------------------------------------------- /.github/workflows/ci-test.yml: -------------------------------------------------------------------------------- 1 | name: CI Test 2 | 3 | on: 4 | workflow_call: 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [16, 18, 19, 20] 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: "yarn" 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | - name: Run linter 29 | run: yarn run lint 30 | - name: Run tests 31 | run: yarn run testonly 32 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | if-dependabot: 9 | if: ${{ github.actor == 'dependabot[bot]' }} 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - run: echo The PR is from dependabot! 15 | 16 | test: 17 | uses: ./.github/workflows/ci-test.yml 18 | 19 | needs: if-dependabot 20 | 21 | dependabot-auto-merge: 22 | runs-on: ubuntu-latest 23 | 24 | needs: test 25 | 26 | permissions: 27 | contents: write # to be able to publish a GitHub release 28 | pull-requests: write # to be able to comment on released pull requests 29 | 30 | steps: 31 | - name: Enable auto-merge for Dependabot PRs 32 | run: gh pr merge --auto --merge "$PR_URL" 33 | env: 34 | PR_URL: ${{github.event.pull_request.html_url}} 35 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 36 | -------------------------------------------------------------------------------- /.github/workflows/publish-package.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | workflow_run: 7 | workflows: ["Dependabot auto-merge"] 8 | types: [completed] 9 | 10 | # Ensure unicity by branch name 11 | concurrency: 12 | group: environment-${{ github.ref }} 13 | cancel-in-progress: false 14 | 15 | jobs: 16 | test: 17 | uses: ./.github/workflows/ci-test.yml 18 | 19 | release: 20 | runs-on: ubuntu-latest 21 | 22 | needs: test 23 | 24 | permissions: 25 | contents: write # to be able to publish a GitHub release 26 | issues: write # to be able to comment on released issues 27 | pull-requests: write # to be able to comment on released pull requests 28 | id-token: write # to enable use of OIDC for npm provenance 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 35 | - name: Setup Node.js 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: "lts/*" 39 | cache: "yarn" 40 | - name: Install dependencies 41 | run: yarn install --frozen-lockfile 42 | - name: Publish package 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_BOT_NPM_TOKEN }} 46 | run: npx semantic-release 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/**/*.flow 3 | coverage/ 4 | yarn-error.log 5 | .DS_Store 6 | *.vscode* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | cache: yarn 4 | branches: 5 | only: 6 | - master 7 | - /^greenkeeper/.*$/ 8 | addons: 9 | code_climate: 10 | repo_token: d8d04f10e1e84541a1b7b15ca5d9c3db46a6d99c2299d8a5bd0c1edffc75870d 11 | before_install: yarn global add greenkeeper-lockfile@1 12 | before_script: greenkeeper-lockfile-update 13 | after_script: greenkeeper-lockfile-upload 14 | after_success: 15 | - yarn global add codeclimate-test-reporter 16 | - codeclimate-test-reporter < ./coverage/lcov.info 17 | notifications: 18 | slack: notifme:MJIOAvkBeUE4bs34W2H9sgaR 19 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 BDav24 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/custom-channel.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../src' 4 | 5 | jest.mock('../src/util/logger', () => ({ 6 | info: jest.fn(), 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | socket: { 12 | to: 'john@example.com', 13 | text: 'Hi John' 14 | } 15 | } 16 | 17 | test.only('socket', async () => { 18 | let socketCalled = false 19 | const sdk = new NotifmeSdk({ 20 | channels: { 21 | socket: { 22 | multiProviderStrategy: 'fallback', 23 | providers: [ 24 | { 25 | type: 'custom', 26 | id: 'my-socket-sender', 27 | send: async () => { 28 | socketCalled = true 29 | return 'custom-socket-id' 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | }) 36 | const result = await sdk.send(request) 37 | 38 | expect(socketCalled).toBe(true) 39 | expect(result).toEqual({ 40 | status: 'success', 41 | channels: { 42 | socket: { 43 | id: 'custom-socket-id', 44 | providerId: 'my-socket-sender' 45 | } 46 | } 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /__tests__/providers/email/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | jest.mock('../../../src/util/logger', () => ({ 6 | info: jest.fn(), 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | email: { 12 | from: 'me@example.com', 13 | to: 'john@example.com', 14 | subject: 'Hi John', 15 | html: 'Hello John! How are you?' 16 | } 17 | } 18 | 19 | test('email unknown provider.', async () => { 20 | const sdk = new NotifmeSdk({ 21 | channels: { 22 | email: { 23 | providers: [{ 24 | type: 'custom', 25 | id: 'my-custom-email-provider', 26 | send: async () => 'custom-returned-id' 27 | }] 28 | } 29 | } 30 | }) 31 | const result = await sdk.send(request) 32 | expect(result).toEqual({ 33 | status: 'success', 34 | channels: { 35 | email: { id: 'custom-returned-id', providerId: 'my-custom-email-provider' } 36 | } 37 | }) 38 | }) 39 | 40 | test('email custom provider.', async () => { 41 | // $FlowIgnore 42 | expect(() => (new NotifmeSdk({ 43 | channels: { 44 | email: { 45 | providers: [{ 46 | type: 'unknown' 47 | }] 48 | } 49 | } 50 | }) 51 | )).toThrow('Unknown email provider "unknown".') 52 | }) 53 | 54 | test('email logger provider.', async () => { 55 | const sdk = new NotifmeSdk({ 56 | channels: { 57 | email: { 58 | providers: [{ 59 | type: 'logger' 60 | }] 61 | } 62 | } 63 | }) 64 | const result = await sdk.send(request) 65 | expect(result).toEqual({ 66 | status: 'success', 67 | channels: { 68 | email: { id: expect.stringContaining('id-'), providerId: 'email-logger-provider' } 69 | } 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /__tests__/providers/email/mailgun.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | email: { 13 | providers: [{ 14 | type: 'mailgun', 15 | apiKey: 'key', 16 | domainName: 'example.com' 17 | }] 18 | } 19 | } 20 | }) 21 | 22 | const request = { 23 | email: { 24 | from: 'me@example.com', 25 | to: 'john@example.com', 26 | subject: 'Hi John', 27 | text: 'Hello John! How are you?' 28 | } 29 | } 30 | 31 | test('Mailgun success with minimal parameters.', async () => { 32 | mockResponse(200, JSON.stringify({ id: 'returned-id' })) 33 | const result = await sdk.send(request) 34 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 35 | hostname: 'api.mailgun.net', 36 | method: 'POST', 37 | path: '/v3/example.com/messages', 38 | protocol: 'https:', 39 | href: 'https://api.mailgun.net/v3/example.com/messages', 40 | headers: expect.objectContaining({ 41 | Accept: ['*/*'], 42 | Authorization: ['Basic YXBpOmtleQ=='], 43 | 'Content-Length': ['530'], 44 | 'Content-Type': [expect.stringContaining('multipart/form-data;boundary=')], 45 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 46 | }) 47 | })) 48 | expect(result).toEqual({ 49 | status: 'success', 50 | channels: { 51 | email: { id: 'returned-id', providerId: 'email-mailgun-provider' } 52 | } 53 | }) 54 | }) 55 | 56 | test('Mailgun success with all parameters.', async () => { 57 | mockResponse(200, JSON.stringify({ id: 'returned-id' })) 58 | const completeRequest = { 59 | metadata: { 60 | id: '24', 61 | userId: '36' 62 | }, 63 | email: { 64 | from: 'from@example.com', 65 | to: 'to@example.com', 66 | subject: 'Hi John', 67 | html: 'Hello John! How are you?', 68 | replyTo: 'replyto@example.com', 69 | headers: { 'My-Custom-Header': 'my-value' }, 70 | cc: ['cc1@example.com', 'cc2@example.com'], 71 | bcc: ['bcc@example.com'], 72 | attachments: [{ 73 | contentType: 'text/plain', 74 | filename: 'test.txt', 75 | content: 'hello!' 76 | }], 77 | customize: async (provider, request) => ({ ...request, subject: 'Hi John!' }) 78 | } 79 | } 80 | const result = await sdk.send(completeRequest) 81 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 82 | hostname: 'api.mailgun.net', 83 | method: 'POST', 84 | path: '/v3/example.com/messages', 85 | protocol: 'https:', 86 | href: 'https://api.mailgun.net/v3/example.com/messages', 87 | headers: expect.objectContaining({ 88 | Accept: ['*/*'], 89 | Authorization: ['Basic YXBpOmtleQ=='], 90 | 'Content-Length': ['1530'], 91 | 'Content-Type': [expect.stringContaining('multipart/form-data;boundary=')], 92 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 93 | }) 94 | })) 95 | expect(result).toEqual({ 96 | status: 'success', 97 | channels: { 98 | email: { id: 'returned-id', providerId: 'email-mailgun-provider' } 99 | } 100 | }) 101 | }) 102 | 103 | test('Mailgun API error.', async () => { 104 | mockResponse(400, JSON.stringify({ message: 'error!' })) 105 | const result = await sdk.send(request) 106 | expect(result).toEqual({ 107 | status: 'error', 108 | errors: { 109 | email: '400 - error!' 110 | }, 111 | channels: { 112 | email: { id: undefined, providerId: 'email-mailgun-provider' } 113 | } 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /__tests__/providers/email/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSend = jest.fn() 6 | jest.mock('../../../src/providers/email/smtp', () => () => ({ 7 | send: mockSend 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | useNotificationCatcher: true 12 | }) 13 | 14 | const request = { 15 | email: { 16 | from: 'me@example.com', 17 | to: 'john@example.com', 18 | subject: 'Hi John', 19 | html: 'Hello John! How are you?', 20 | text: 'Hello John! How are you?', 21 | replyTo: 'contact@example.com' 22 | } 23 | } 24 | 25 | test('email notification catcher provider should use SMTP provider.', async () => { 26 | const result = await sdk.send(request) 27 | const { to, from, html, text, subject, replyTo } = request.email 28 | expect(mockSend).lastCalledWith({ 29 | to, from, html, text, subject, replyTo, headers: { 'X-to': `[email] ${to}` } 30 | }) 31 | expect(result).toEqual({ 32 | status: 'success', 33 | channels: { 34 | email: { id: undefined, providerId: 'email-notificationcatcher-provider' } 35 | } 36 | }) 37 | }) 38 | 39 | test('email notification catcher provider should customize requests.', async () => { 40 | await sdk.send({ 41 | email: { 42 | ...request.email, 43 | customize: async (provider, request) => ({ ...request, subject: 'Hi John!' }) 44 | } 45 | }) 46 | const { to, from, html, text, replyTo } = request.email 47 | expect(mockSend).lastCalledWith({ 48 | to, from, html, text, subject: 'Hi John!', replyTo, headers: { 'X-to': `[email] ${to}` } 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /__tests__/providers/email/sendmail.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSendMail = jest.fn() 6 | mockSendMail.mockReturnValue({ messageId: 'returned-id' }) 7 | jest.mock('nodemailer', () => ({ 8 | createTransport: () => ({ 9 | sendMail: mockSendMail 10 | }) 11 | })) 12 | jest.mock('../../../src/util/logger', () => ({ 13 | warn: jest.fn() 14 | })) 15 | 16 | const sdk = new NotifmeSdk({ 17 | channels: { 18 | email: { 19 | providers: [{ 20 | type: 'sendmail', 21 | sendmail: true, 22 | path: 'sendmail', 23 | newline: 'unix' 24 | }] 25 | } 26 | } 27 | }) 28 | 29 | const request = { 30 | email: { 31 | from: 'me@example.com', 32 | to: 'john@example.com', 33 | subject: 'Hi John', 34 | text: 'Hello John! How are you?' 35 | } 36 | } 37 | 38 | test('Sendmail should use nodemailer.', async () => { 39 | const result = await sdk.send(request) 40 | expect(mockSendMail).lastCalledWith(request.email) 41 | expect(result).toEqual({ 42 | status: 'success', 43 | channels: { 44 | email: { id: 'returned-id', providerId: 'email-sendmail-provider' } 45 | } 46 | }) 47 | }) 48 | 49 | test('Sendmail should customize requests.', async () => { 50 | await sdk.send({ 51 | email: { 52 | ...request.email, 53 | customize: async (provider, request) => ({ ...request, subject: 'Hi John!' }) 54 | } 55 | }) 56 | expect(mockSendMail).lastCalledWith({ ...request.email, subject: 'Hi John!' }) 57 | }) 58 | -------------------------------------------------------------------------------- /__tests__/providers/email/smtp.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSendMail = jest.fn() 6 | mockSendMail.mockReturnValue({ messageId: 'returned-id' }) 7 | jest.mock('nodemailer', () => ({ 8 | createTransport: () => ({ 9 | sendMail: mockSendMail 10 | }) 11 | })) 12 | jest.mock('../../../src/util/logger', () => ({ 13 | warn: jest.fn() 14 | })) 15 | 16 | const sdk = new NotifmeSdk({ 17 | channels: { 18 | email: { 19 | providers: [{ 20 | type: 'smtp', 21 | auth: { 22 | user: 'user', 23 | pass: 'password' 24 | } 25 | }] 26 | } 27 | } 28 | }) 29 | 30 | const request = { 31 | email: { 32 | from: 'me@example.com', 33 | to: 'john@example.com', 34 | subject: 'Hi John', 35 | text: 'Hello John! How are you?' 36 | } 37 | } 38 | 39 | test('Smtp should use nodemailer.', async () => { 40 | const result = await sdk.send(request) 41 | expect(mockSendMail).lastCalledWith(request.email) 42 | expect(result).toEqual({ 43 | status: 'success', 44 | channels: { 45 | email: { id: 'returned-id', providerId: 'email-smtp-provider' } 46 | } 47 | }) 48 | }) 49 | 50 | test('Smtp should customize requests.', async () => { 51 | await sdk.send({ 52 | email: { 53 | ...request.email, 54 | customize: async (provider, request) => ({ ...request, subject: 'Hi John!' }) 55 | } 56 | }) 57 | expect(mockSendMail).lastCalledWith({ ...request.email, subject: 'Hi John!' }) 58 | }) 59 | -------------------------------------------------------------------------------- /__tests__/providers/mockHttp.js: -------------------------------------------------------------------------------- 1 | /* global jest, test */ 2 | import https from 'https' 3 | import EventEmitter from 'events' 4 | 5 | const mockHttp = jest.fn() 6 | https.request = mockHttp 7 | 8 | export default mockHttp 9 | 10 | export function mockResponse (statusCode: number, body: string) { 11 | const mockRequest = new EventEmitter() 12 | mockRequest.write = (body) => (mockHttp.body = body.toString('utf8')) 13 | mockRequest.end = () => mockRequest.emit('response', { 14 | statusCode, 15 | headers: {}, 16 | pipe: () => body, 17 | once: () => {} 18 | }) 19 | https.request.mockReturnValue(mockRequest) 20 | } 21 | 22 | test('not a test', () => {}) 23 | -------------------------------------------------------------------------------- /__tests__/providers/slack/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | jest.mock('../../../src/util/logger', () => ({ 6 | info: jest.fn(), 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | slack: { 12 | text: 'Hello John! How are you?' 13 | } 14 | } 15 | 16 | test('slack unknown provider.', async () => { 17 | const sdk = new NotifmeSdk({ 18 | channels: { 19 | slack: { 20 | providers: [{ 21 | type: 'custom', 22 | id: 'my-custom-slack-provider', 23 | send: async () => 'custom-returned-id' 24 | }] 25 | } 26 | } 27 | }) 28 | const result = await sdk.send(request) 29 | expect(result).toEqual({ 30 | status: 'success', 31 | channels: { 32 | slack: { id: 'custom-returned-id', providerId: 'my-custom-slack-provider' } 33 | } 34 | }) 35 | }) 36 | 37 | test('slack custom provider.', async () => { 38 | // $FlowIgnore 39 | expect(() => (new NotifmeSdk({ 40 | channels: { 41 | slack: { 42 | providers: [{ 43 | type: 'unknown' 44 | }] 45 | } 46 | } 47 | }) 48 | )).toThrow('Unknown slack provider "unknown".') 49 | }) 50 | 51 | test('slack logger provider.', async () => { 52 | const sdk = new NotifmeSdk({ 53 | channels: { 54 | slack: { 55 | providers: [{ 56 | type: 'logger' 57 | }] 58 | } 59 | } 60 | }) 61 | const result = await sdk.send(request) 62 | expect(result).toEqual({ 63 | status: 'success', 64 | channels: { 65 | slack: { id: expect.stringContaining('id-'), providerId: 'slack-logger-provider' } 66 | } 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /__tests__/providers/slack/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSend = jest.fn() 6 | jest.mock('../../../src/providers/email/smtp', () => () => ({ 7 | send: mockSend 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | useNotificationCatcher: true 12 | }) 13 | 14 | test('slack notification catcher provider should use SMTP provider.', async () => { 15 | const result = await sdk.send({ 16 | slack: { 17 | text: 'Hello John!' 18 | } 19 | }) 20 | expect(mockSend).lastCalledWith({ 21 | to: 'public.channel@slack', 22 | from: '-', 23 | subject: 'Hello John!', 24 | text: 'Hello John!', 25 | headers: { 26 | 'X-type': 'slack', 27 | 'X-to': '[slack public channel]' 28 | } 29 | }) 30 | expect(result).toEqual({ 31 | status: 'success', 32 | channels: { 33 | slack: { id: '', providerId: 'slack-notificationcatcher-provider' } 34 | } 35 | }) 36 | }) 37 | 38 | test('slack notification catcher provider should use SMTP provider (long message).', async () => { 39 | const result = await sdk.send({ 40 | slack: { 41 | text: 'Hello John! How are you?' 42 | } 43 | }) 44 | expect(mockSend).lastCalledWith({ 45 | to: 'public.channel@slack', 46 | from: '-', 47 | subject: 'Hello John! How are ...', 48 | text: 'Hello John! How are you?', 49 | headers: { 50 | 'X-type': 'slack', 51 | 'X-to': '[slack public channel]' 52 | } 53 | }) 54 | expect(result).toEqual({ 55 | status: 'success', 56 | channels: { 57 | slack: { id: '', providerId: 'slack-notificationcatcher-provider' } 58 | } 59 | }) 60 | }) 61 | 62 | test('slack customized success.', async () => { 63 | await sdk.send({ 64 | slack: { 65 | text: '', 66 | customize: async (provider, request) => ({ text: 'Hello John! How are you?' }) 67 | } 68 | }) 69 | expect(mockSend).lastCalledWith({ 70 | to: 'public.channel@slack', 71 | from: '-', 72 | subject: 'Hello John! How are ...', 73 | text: 'Hello John! How are you?', 74 | headers: { 75 | 'X-type': 'slack', 76 | 'X-to': '[slack public channel]' 77 | } 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /__tests__/providers/slack/slack.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | slack: { 13 | providers: [{ 14 | type: 'webhook', 15 | webhookUrl: 'https://hooks.slack.com/services/Txxxxxxxx/Bxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx' 16 | }] 17 | } 18 | } 19 | }) 20 | 21 | const request = { 22 | slack: { 23 | text: 'Hello John! How are you?' 24 | } 25 | } 26 | 27 | test('Slack success.', async () => { 28 | mockResponse(200, 'ok') 29 | const result = await sdk.send(request) 30 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 31 | method: 'POST', 32 | href: 'https://hooks.slack.com/services/Txxxxxxxx/Bxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx' 33 | })) 34 | expect(mockHttp.body).toContain( 35 | '{"text":"Hello John! How are you?"}' 36 | ) 37 | expect(result).toEqual({ 38 | status: 'success', 39 | channels: { 40 | slack: { id: '', providerId: 'slack-provider' } 41 | } 42 | }) 43 | }) 44 | 45 | test('Slack customized success.', async () => { 46 | mockResponse(200, 'ok') 47 | await sdk.send({ 48 | slack: { 49 | text: '', 50 | customize: async (provider, request) => ({ text: 'Hello John! How are you?' }) 51 | } 52 | }) 53 | expect(mockHttp.body).toContain( 54 | '{"text":"Hello John! How are you?"}' 55 | ) 56 | }) 57 | 58 | test('Slack with no message.', async () => { 59 | mockResponse(500, 'missing_text_or_fallback_or_attachments') 60 | // $FlowIgnore 61 | const result = await sdk.send({ slack: { text: [] } }) 62 | expect(result).toEqual({ 63 | status: 'error', 64 | errors: { 65 | slack: '500 - missing_text_or_fallback_or_attachments' 66 | }, 67 | channels: { 68 | slack: { id: undefined, providerId: 'slack-provider' } 69 | } 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /__tests__/providers/sms/46elks.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | sms: { 13 | providers: [{ 14 | type: '46elks', 15 | apiUsername: 'username', 16 | apiPassword: 'password' 17 | }] 18 | } 19 | } 20 | }) 21 | 22 | const request = { 23 | sms: { 24 | from: 'Notifme', 25 | to: '+15000000001', 26 | text: 'Hello John! How are you?' 27 | } 28 | } 29 | 30 | test('46Elks success with minimal parameters.', async () => { 31 | mockResponse(200, JSON.stringify({ id: 'returned-id' })) 32 | const result = await sdk.send(request) 33 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 34 | hostname: 'api.46elks.com', 35 | method: 'POST', 36 | path: '/a1/sms', 37 | protocol: 'https:', 38 | href: 'https://api.46elks.com/a1/sms', 39 | headers: expect.objectContaining({ 40 | Accept: ['*/*'], 41 | Authorization: ['Basic dXNlcm5hbWU6cGFzc3dvcmQ='], 42 | 'Content-Length': ['73'], 43 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 44 | }) 45 | })) 46 | expect(mockHttp.body).toEqual( 47 | 'from=Notifme&to=%2B15000000001&message=Hello%20John!%20How%20are%20you%3F' 48 | ) 49 | expect(result).toEqual({ 50 | status: 'success', 51 | channels: { 52 | sms: { id: 'returned-id', providerId: 'sms-46elks-provider' } 53 | } 54 | }) 55 | }) 56 | 57 | test('46Elks shouls customize requests.', async () => { 58 | mockResponse(200, JSON.stringify({ id: 'returned-id' })) 59 | await sdk.send({ 60 | sms: { 61 | ...request.sms, 62 | customize: async (provider, request) => ({ ...request, text: 'Hello John! How are you??' }) 63 | } 64 | }) 65 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 66 | headers: expect.objectContaining({ 'Content-Length': ['76'] }) 67 | })) 68 | }) 69 | 70 | test('46Elks error.', async () => { 71 | mockResponse(400, 'error!') 72 | const result = await sdk.send(request) 73 | expect(result).toEqual({ 74 | status: 'error', 75 | errors: { 76 | sms: 'error!' 77 | }, 78 | channels: { 79 | sms: { id: undefined, providerId: 'sms-46elks-provider' } 80 | } 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /__tests__/providers/sms/callr.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | sms: { 13 | providers: [{ 14 | type: 'callr', 15 | login: 'login', 16 | password: 'password' 17 | }] 18 | } 19 | } 20 | }) 21 | 22 | const request = { 23 | sms: { from: 'Notifme', to: '+15000000001', text: 'Hello John! How are you?' } 24 | } 25 | 26 | test('Callr success with minimal parameters.', async () => { 27 | mockResponse(200, JSON.stringify({ data: 'returned-id' })) 28 | const result = await sdk.send(request) 29 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 30 | hostname: 'api.callr.com', 31 | method: 'POST', 32 | path: '/rest/v1.1/sms', 33 | protocol: 'https:', 34 | href: 'https://api.callr.com/rest/v1.1/sms', 35 | headers: expect.objectContaining({ 36 | Accept: ['*/*'], 37 | Authorization: ['Basic bG9naW46cGFzc3dvcmQ='], 38 | 'Content-Length': ['127'], 39 | 'Content-Type': ['application/json'], 40 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 41 | }) 42 | })) 43 | expect(mockHttp.body).toEqual( 44 | '{"from":"Notifme","to":"+15000000001","body":"Hello John! How are you?","options":{"force_encoding":"GSM","nature":"ALERTING"}}' 45 | ) 46 | expect(result).toEqual({ 47 | status: 'success', 48 | channels: { 49 | sms: { id: 'returned-id', providerId: 'sms-callr-provider' } 50 | } 51 | }) 52 | }) 53 | 54 | test('Callr success with all parameters.', async () => { 55 | mockResponse(200, JSON.stringify({ data: 'returned-id' })) 56 | const completeRequest = { 57 | metadata: { id: '24' }, 58 | sms: { 59 | from: 'Notifme', 60 | to: '+15000000001', 61 | text: 'Hello John! How are you?', 62 | type: 'unicode', 63 | nature: 'marketing', 64 | ttl: 3600, 65 | messageClass: 1, 66 | customize: async (provider, request) => ({ ...request, text: 'Hello John! How are you??' }) 67 | } 68 | } 69 | const result = await sdk.send(completeRequest) 70 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 71 | hostname: 'api.callr.com', 72 | method: 'POST', 73 | path: '/rest/v1.1/sms', 74 | protocol: 'https:', 75 | href: 'https://api.callr.com/rest/v1.1/sms', 76 | headers: expect.objectContaining({ 77 | Accept: ['*/*'], 78 | Authorization: ['Basic bG9naW46cGFzc3dvcmQ='], 79 | 'Content-Length': ['150'], 80 | 'Content-Type': ['application/json'], 81 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 82 | }) 83 | })) 84 | expect(mockHttp.body).toEqual( 85 | '{"from":"Notifme","to":"+15000000001","body":"Hello John! How are you??","options":{"force_encoding":"UNICODE","nature":"MARKETING","user_data":"24"}}' 86 | ) 87 | expect(result).toEqual({ 88 | status: 'success', 89 | channels: { 90 | sms: { id: 'returned-id', providerId: 'sms-callr-provider' } 91 | } 92 | }) 93 | }) 94 | 95 | test('Callr error.', async () => { 96 | mockResponse(400, JSON.stringify({ data: { code: '400', message: 'error!' } })) 97 | const result = await sdk.send(request) 98 | expect(result).toEqual({ 99 | status: 'error', 100 | errors: { 101 | sms: 'code: 400, message: error!' 102 | }, 103 | channels: { 104 | sms: { id: undefined, providerId: 'sms-callr-provider' } 105 | } 106 | }) 107 | }) 108 | -------------------------------------------------------------------------------- /__tests__/providers/sms/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | jest.mock('../../../src/util/logger', () => ({ 6 | info: jest.fn(), 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | sms: { from: 'Notifme', to: '+15000000001', text: 'Hello John! How are you?' } 12 | } 13 | 14 | test('sms unknown provider.', async () => { 15 | const sdk = new NotifmeSdk({ 16 | channels: { 17 | sms: { 18 | providers: [{ 19 | type: 'custom', 20 | id: 'my-custom-sms-provider', 21 | send: async () => 'custom-returned-id' 22 | }] 23 | } 24 | } 25 | }) 26 | const result = await sdk.send(request) 27 | expect(result).toEqual({ 28 | status: 'success', 29 | channels: { 30 | sms: { id: 'custom-returned-id', providerId: 'my-custom-sms-provider' } 31 | } 32 | }) 33 | }) 34 | 35 | test('sms custom provider.', async () => { 36 | // $FlowIgnore 37 | expect(() => (new NotifmeSdk({ 38 | channels: { 39 | sms: { 40 | providers: [{ 41 | type: 'unknown' 42 | }] 43 | } 44 | } 45 | }) 46 | )).toThrow('Unknown sms provider "unknown".') 47 | }) 48 | 49 | test('sms logger provider.', async () => { 50 | const sdk = new NotifmeSdk({ 51 | channels: { 52 | sms: { 53 | providers: [{ 54 | type: 'logger' 55 | }] 56 | } 57 | } 58 | }) 59 | const result = await sdk.send(request) 60 | expect(result).toEqual({ 61 | status: 'success', 62 | channels: { 63 | sms: { id: expect.stringContaining('id-'), providerId: 'sms-logger-provider' } 64 | } 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /__tests__/providers/sms/nexmo.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | sms: { 13 | providers: [{ 14 | type: 'nexmo', 15 | apiKey: 'key', 16 | apiSecret: 'secret' 17 | }] 18 | } 19 | } 20 | }) 21 | 22 | const request = { 23 | sms: { 24 | from: 'Notifme', 25 | to: '+15000000001', 26 | text: 'Hello John! How are you?' 27 | } 28 | } 29 | 30 | test('Nexmo success with minimal parameters.', async () => { 31 | mockResponse(200, JSON.stringify({ messages: [{ status: '0', 'message-id': 'returned-id' }] })) 32 | const result = await sdk.send({ 33 | sms: { 34 | from: 'Notifme', 35 | to: '+15000000001', 36 | text: 'Hello John! How are you?', 37 | customize: async (provider, request) => ({ ...request, text: 'Hello John! How are you??' }) 38 | } 39 | }) 40 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 41 | hostname: 'rest.nexmo.com', 42 | method: 'POST', 43 | path: '/sms/json', 44 | protocol: 'https:', 45 | href: 'https://rest.nexmo.com/sms/json', 46 | headers: expect.objectContaining({ 47 | Accept: ['*/*'], 48 | 'Content-Length': ['111'], 49 | 'Content-Type': ['application/json'], 50 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 51 | }) 52 | })) 53 | expect(mockHttp.body).toEqual( 54 | '{"api_key":"key","api_secret":"secret","from":"Notifme","to":"+15000000001","text":"Hello John! How are you??"}' 55 | ) 56 | expect(result).toEqual({ 57 | status: 'success', 58 | channels: { 59 | sms: { id: 'returned-id', providerId: 'sms-nexmo-provider' } 60 | } 61 | }) 62 | }) 63 | 64 | test('Nexmo success with all parameters.', async () => { 65 | mockResponse(200, JSON.stringify({ messages: [{ status: '0', 'message-id': 'returned-id' }] })) 66 | const result = await sdk.send(request) 67 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 68 | hostname: 'rest.nexmo.com', 69 | method: 'POST', 70 | path: '/sms/json', 71 | protocol: 'https:', 72 | href: 'https://rest.nexmo.com/sms/json', 73 | headers: expect.objectContaining({ 74 | Accept: ['*/*'], 75 | 'Content-Length': ['110'], 76 | 'Content-Type': ['application/json'], 77 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 78 | }) 79 | })) 80 | expect(mockHttp.body).toEqual( 81 | '{"api_key":"key","api_secret":"secret","from":"Notifme","to":"+15000000001","text":"Hello John! How are you?"}' 82 | ) 83 | expect(result).toEqual({ 84 | status: 'success', 85 | channels: { 86 | sms: { id: 'returned-id', providerId: 'sms-nexmo-provider' } 87 | } 88 | }) 89 | }) 90 | 91 | test('Nexmo API error.', async () => { 92 | mockResponse(400, '') 93 | const result = await sdk.send(request) 94 | expect(result).toEqual({ 95 | status: 'error', 96 | errors: { 97 | sms: '400' 98 | }, 99 | channels: { 100 | sms: { id: undefined, providerId: 'sms-nexmo-provider' } 101 | } 102 | }) 103 | }) 104 | 105 | test('Nexmo error.', async () => { 106 | mockResponse(200, JSON.stringify({ messages: [{ status: '1', 'error-text': 'error!' }] })) 107 | const result = await sdk.send(request) 108 | expect(result).toEqual({ 109 | status: 'error', 110 | errors: { 111 | sms: 'status: 1, error: error!' 112 | }, 113 | channels: { 114 | sms: { id: undefined, providerId: 'sms-nexmo-provider' } 115 | } 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /__tests__/providers/sms/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSend = jest.fn() 6 | jest.mock('../../../src/providers/email/smtp', () => () => ({ 7 | send: mockSend 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | useNotificationCatcher: true 12 | }) 13 | 14 | const request = { 15 | sms: { from: 'Notifme', to: '+15000000001', text: 'Hello John!' } 16 | } 17 | 18 | test('sms notification catcher provider should use SMTP provider.', async () => { 19 | const result = await sdk.send(request) 20 | expect(mockSend).lastCalledWith({ 21 | from: 'Notifme', 22 | headers: { 23 | 'X-to': '[sms] +15000000001', 24 | 'X-type': 'sms' 25 | }, 26 | subject: 'Hello John!', 27 | to: '+15000000001@sms', 28 | text: 'Hello John!' 29 | }) 30 | expect(result).toEqual({ 31 | status: 'success', 32 | channels: { 33 | sms: { id: undefined, providerId: 'sms-notificationcatcher-provider' } 34 | } 35 | }) 36 | }) 37 | 38 | test('sms notification catcher provider should use SMTP provider (long message).', async () => { 39 | const result = await sdk.send({ 40 | sms: { 41 | ...request.sms, 42 | customize: async (provider, request) => ({ ...request, text: 'very very very very very very very very long' }) 43 | } 44 | }) 45 | expect(mockSend).lastCalledWith({ 46 | from: 'Notifme', 47 | headers: { 48 | 'X-to': '[sms] +15000000001', 49 | 'X-type': 'sms' 50 | }, 51 | subject: 'very very very very ...', 52 | to: '+15000000001@sms', 53 | text: 'very very very very very very very very long' 54 | }) 55 | expect(result).toEqual({ 56 | status: 'success', 57 | channels: { 58 | sms: { id: undefined, providerId: 'sms-notificationcatcher-provider' } 59 | } 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /__tests__/providers/sms/plivo.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | sms: { 13 | providers: [{ 14 | type: 'plivo', 15 | authId: 'id', 16 | authToken: 'token' 17 | }] 18 | } 19 | } 20 | }) 21 | 22 | const request = { 23 | sms: { from: 'Notifme', to: '+15000000001', text: 'Hello John! How are you?' } 24 | } 25 | 26 | test('Plivo success with minimal parameters.', async () => { 27 | mockResponse(200, JSON.stringify({ message_uuid: ['returned-id'] })) 28 | const result = await sdk.send(request) 29 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 30 | hostname: 'api.plivo.com', 31 | method: 'POST', 32 | path: '/v1/Account/id/Message/', 33 | protocol: 'https:', 34 | href: 'https://api.plivo.com/v1/Account/id/Message/', 35 | headers: expect.objectContaining({ 36 | Accept: ['*/*'], 37 | Authorization: ['Basic aWQ6dG9rZW4='], 38 | 'Content-Length': ['72'], 39 | 'Content-Type': ['application/json'], 40 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 41 | }) 42 | })) 43 | expect(mockHttp.body).toEqual( 44 | '{"src":"Notifme","dst":"+15000000001","text":"Hello John! How are you?"}' 45 | ) 46 | expect(result).toEqual({ 47 | status: 'success', 48 | channels: { 49 | sms: { id: 'returned-id', providerId: 'sms-plivo-provider' } 50 | } 51 | }) 52 | }) 53 | 54 | test('Plivo success with all parameters.', async () => { 55 | mockResponse(200, JSON.stringify({ message_uuid: ['returned-id'] })) 56 | const result = await sdk.send({ 57 | sms: { 58 | from: 'Notifme', 59 | to: '+15000000001', 60 | text: '', 61 | customize: async (provider, request) => ({ ...request, text: 'Hello John! How are you??' }) 62 | } 63 | }) 64 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 65 | hostname: 'api.plivo.com', 66 | method: 'POST', 67 | path: '/v1/Account/id/Message/', 68 | protocol: 'https:', 69 | href: 'https://api.plivo.com/v1/Account/id/Message/', 70 | headers: expect.objectContaining({ 71 | Accept: ['*/*'], 72 | Authorization: ['Basic aWQ6dG9rZW4='], 73 | 'Content-Length': ['73'], 74 | 'Content-Type': ['application/json'], 75 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 76 | }) 77 | })) 78 | expect(mockHttp.body).toEqual( 79 | '{"src":"Notifme","dst":"+15000000001","text":"Hello John! How are you??"}' 80 | ) 81 | expect(result).toEqual({ 82 | status: 'success', 83 | channels: { 84 | sms: { id: 'returned-id', providerId: 'sms-plivo-provider' } 85 | } 86 | }) 87 | }) 88 | 89 | test('Plivo API unauthorized error.', async () => { 90 | mockResponse(401, 'unauthorized') 91 | const result = await sdk.send(request) 92 | expect(result).toEqual({ 93 | status: 'error', 94 | errors: { 95 | sms: 'unauthorized' 96 | }, 97 | channels: { 98 | sms: { id: undefined, providerId: 'sms-plivo-provider' } 99 | } 100 | }) 101 | }) 102 | 103 | test('Plivo API error.', async () => { 104 | mockResponse(400, JSON.stringify({ error: 'error!' })) 105 | const result = await sdk.send(request) 106 | expect(result).toEqual({ 107 | status: 'error', 108 | errors: { 109 | sms: 'error!' 110 | }, 111 | channels: { 112 | sms: { id: undefined, providerId: 'sms-plivo-provider' } 113 | } 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /__tests__/providers/sms/seven.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | sms: { 13 | providers: [{ 14 | type: 'seven', 15 | apiKey: 'apiKey' 16 | }] 17 | } 18 | } 19 | }) 20 | 21 | const request = { 22 | sms: { 23 | from: 'Notifme', 24 | to: '+15000000001', 25 | text: 'Hello John! How are you?' 26 | } 27 | } 28 | 29 | const apiResponse = { 30 | balance: 46.748, 31 | debug: 'false', 32 | messages: [ 33 | { 34 | encoding: 'gsm', 35 | error: null, 36 | error_text: null, 37 | id: 'returned-id', 38 | is_binary: false, 39 | label: null, 40 | parts: 1, 41 | price: 0.075, 42 | recipient: '491716992343', 43 | sender: '491716992343', 44 | success: true, 45 | text: 'text', 46 | udh: null 47 | } 48 | ], 49 | sms_type: 'direct', 50 | success: '100', 51 | total_price: 0.075 52 | } 53 | 54 | test('seven success with minimal parameters.', async () => { 55 | mockResponse(200, JSON.stringify(apiResponse)) 56 | const result = await sdk.send(request) 57 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 58 | hostname: 'gateway.seven.io', 59 | method: 'POST', 60 | path: '/api/sms', 61 | protocol: 'https:', 62 | href: 'https://gateway.seven.io/api/sms', 63 | headers: expect.objectContaining({ 64 | Accept: ['application/json'], 65 | 'Content-Length': ['94'], 66 | 'Content-Type': ['application/json'], 67 | SentWith: ['Notifme'], 68 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'], 69 | 'X-Api-Key': ['apiKey'] 70 | }) 71 | })) 72 | expect(mockHttp.body).toEqual( 73 | '{"flash":0,"from":"Notifme","text":"Hello John! How are you?","to":"+15000000001","unicode":0}' 74 | ) 75 | expect(result).toEqual({ 76 | status: 'success', 77 | channels: { 78 | sms: { id: 'returned-id', providerId: 'sms-seven-provider' } 79 | } 80 | }) 81 | }) 82 | 83 | test('seven should customize requests.', async () => { 84 | mockResponse(200, JSON.stringify(apiResponse)) 85 | await sdk.send({ 86 | sms: { 87 | ...request.sms, 88 | customize: async (provider, request) => ({ 89 | ...request, 90 | text: 'a totally new message', 91 | messageClass: 0, 92 | type: 'unicode' 93 | }) 94 | } 95 | }) 96 | expect(mockHttp.body).toEqual( 97 | '{"flash":1,"from":"Notifme","text":"a totally new message","to":"+15000000001","unicode":1}' 98 | ) 99 | }) 100 | 101 | test('seven error.', async () => { 102 | mockResponse(400, 'error!') 103 | const result = await sdk.send(request) 104 | expect(result).toEqual({ 105 | status: 'error', 106 | errors: { 107 | sms: 'error!' 108 | }, 109 | channels: { 110 | sms: { id: undefined, providerId: 'sms-seven-provider' } 111 | } 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /__tests__/providers/sms/twilio.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | sms: { 13 | providers: [{ 14 | type: 'twilio', 15 | accountSid: 'account', 16 | authToken: 'token' 17 | }] 18 | } 19 | } 20 | }) 21 | 22 | const request = { 23 | sms: { from: 'Notifme', to: '+15000000001', text: 'Hello John! How are you?' } 24 | } 25 | 26 | test('Twilio success with minimal parameters.', async () => { 27 | mockResponse(200, JSON.stringify({ sid: 'returned-id' })) 28 | const result = await sdk.send(request) 29 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 30 | hostname: 'api.twilio.com', 31 | method: 'POST', 32 | path: '/2010-04-01/Accounts/account/Messages.json', 33 | protocol: 'https:', 34 | href: 'https://api.twilio.com/2010-04-01/Accounts/account/Messages.json', 35 | headers: expect.objectContaining({ 36 | Accept: ['*/*'], 37 | Authorization: ['Basic YWNjb3VudDp0b2tlbg=='], 38 | 'Content-Length': ['406'], 39 | 'Content-Type': [expect.stringContaining('multipart/form-data;boundary=')], 40 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 41 | }) 42 | })) 43 | expect(result).toEqual({ 44 | status: 'success', 45 | channels: { 46 | sms: { id: 'returned-id', providerId: 'sms-twilio-provider' } 47 | } 48 | }) 49 | }) 50 | 51 | test('Twilio success with all parameters.', async () => { 52 | mockResponse(200, JSON.stringify({ sid: 'returned-id' })) 53 | const completeRequest = { 54 | metadata: { id: '24' }, 55 | sms: { 56 | from: 'Notifme', 57 | to: '+15000000001', 58 | text: 'Hello John! How are you?', 59 | type: 'unicode', 60 | nature: 'marketing', 61 | ttl: 3600, 62 | messageClass: 1, 63 | customize: async (provider, request) => ({ ...request, text: 'Hello John! How are you??' }) 64 | } 65 | } 66 | const result = await sdk.send(completeRequest) 67 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 68 | hostname: 'api.twilio.com', 69 | method: 'POST', 70 | path: '/2010-04-01/Accounts/account/Messages.json', 71 | protocol: 'https:', 72 | href: 'https://api.twilio.com/2010-04-01/Accounts/account/Messages.json', 73 | headers: expect.objectContaining({ 74 | Accept: ['*/*'], 75 | Authorization: ['Basic YWNjb3VudDp0b2tlbg=='], 76 | 'Content-Length': ['524'], 77 | 'Content-Type': [expect.stringContaining('multipart/form-data;boundary=')], 78 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 79 | }) 80 | })) 81 | expect(result).toEqual({ 82 | status: 'success', 83 | channels: { 84 | sms: { id: 'returned-id', providerId: 'sms-twilio-provider' } 85 | } 86 | }) 87 | }) 88 | 89 | test('Twilio API error.', async () => { 90 | mockResponse(400, JSON.stringify({ message: 'error!' })) 91 | const result = await sdk.send(request) 92 | expect(result).toEqual({ 93 | status: 'error', 94 | errors: { 95 | sms: '400 - error!' 96 | }, 97 | channels: { 98 | sms: { id: undefined, providerId: 'sms-twilio-provider' } 99 | } 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /__tests__/providers/voice/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | jest.mock('../../../src/util/logger', () => ({ 6 | info: jest.fn(), 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | voice: { from: 'Notifme', to: '+15000000001', url: 'https://notifme.github.io' } 12 | } 13 | 14 | test('voice unknown provider.', async () => { 15 | const sdk = new NotifmeSdk({ 16 | channels: { 17 | voice: { 18 | providers: [{ 19 | type: 'custom', 20 | id: 'my-custom-voice-provider', 21 | send: async () => 'custom-returned-id' 22 | }] 23 | } 24 | } 25 | }) 26 | const result = await sdk.send(request) 27 | expect(result).toEqual({ 28 | status: 'success', 29 | channels: { 30 | voice: { id: 'custom-returned-id', providerId: 'my-custom-voice-provider' } 31 | } 32 | }) 33 | }) 34 | 35 | test('voice custom provider.', async () => { 36 | // $FlowIgnore 37 | expect(() => (new NotifmeSdk({ 38 | channels: { 39 | voice: { 40 | providers: [{ 41 | type: 'unknown' 42 | }] 43 | } 44 | } 45 | }) 46 | )).toThrow('Unknown voice provider "unknown".') 47 | }) 48 | 49 | test('voice logger provider.', async () => { 50 | const sdk = new NotifmeSdk({ 51 | channels: { 52 | voice: { 53 | providers: [{ 54 | type: 'logger' 55 | }] 56 | } 57 | } 58 | }) 59 | const result = await sdk.send(request) 60 | expect(result).toEqual({ 61 | status: 'success', 62 | channels: { 63 | voice: { id: expect.stringContaining('id-'), providerId: 'voice-logger-provider' } 64 | } 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /__tests__/providers/voice/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSend = jest.fn() 6 | jest.mock('../../../src/providers/email/smtp', () => () => ({ 7 | send: mockSend 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | useNotificationCatcher: true 12 | }) 13 | 14 | const request = { 15 | voice: { from: 'Notifme', to: '+15000000001', url: 'https://notifme.github.io' } 16 | } 17 | 18 | test('voice notification catcher provider should use SMTP provider.', async () => { 19 | const result = await sdk.send(request) 20 | expect(mockSend).lastCalledWith({ 21 | from: 'Notifme', 22 | headers: { 23 | 'X-to': '[voice] +15000000001', 24 | 'X-type': 'voice' 25 | }, 26 | subject: '+15000000001@voice', 27 | to: '+15000000001@voice', 28 | text: 'https://notifme.github.io' 29 | }) 30 | expect(result).toEqual({ 31 | status: 'success', 32 | channels: { 33 | voice: { id: undefined, providerId: 'voice-notificationcatcher-provider' } 34 | } 35 | }) 36 | }) 37 | 38 | test('voice notification catcher provider should customize requests.', async () => { 39 | await sdk.send({ 40 | voice: { 41 | ...request.voice, 42 | customize: async (provider, request) => ({ ...request, url: 'url...' }) 43 | } 44 | }) 45 | expect(mockSend).lastCalledWith({ 46 | from: 'Notifme', 47 | headers: { 48 | 'X-to': '[voice] +15000000001', 49 | 'X-type': 'voice' 50 | }, 51 | subject: '+15000000001@voice', 52 | to: '+15000000001@voice', 53 | text: 'url...' 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /__tests__/providers/voice/twilio.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import mockHttp, { mockResponse } from '../mockHttp' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | channels: { 12 | voice: { 13 | providers: [{ 14 | type: 'twilio', 15 | accountSid: 'account', 16 | authToken: 'token' 17 | }] 18 | } 19 | } 20 | }) 21 | 22 | const request = { 23 | voice: { from: 'Notifme', to: '+15000000001', url: 'https://notifme.github.io' } 24 | } 25 | 26 | test('Twilio success with minimal parameters.', async () => { 27 | mockResponse(200, JSON.stringify({ sid: 'returned-id' })) 28 | const result = await sdk.send(request) 29 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 30 | hostname: 'api.twilio.com', 31 | method: 'POST', 32 | path: '/2010-04-01/Accounts/account/Calls.json', 33 | protocol: 'https:', 34 | href: 'https://api.twilio.com/2010-04-01/Accounts/account/Calls.json', 35 | headers: expect.objectContaining({ 36 | Accept: ['*/*'], 37 | Authorization: ['Basic YWNjb3VudDp0b2tlbg=='], 38 | 'Content-Type': [expect.stringContaining('multipart/form-data;boundary=')], 39 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 40 | }) 41 | })) 42 | expect(result).toEqual({ 43 | status: 'success', 44 | channels: { 45 | voice: { id: 'returned-id', providerId: 'voice-twilio-provider' } 46 | } 47 | }) 48 | }) 49 | 50 | test('Twilio success with all parameters.', async () => { 51 | mockResponse(200, JSON.stringify({ sid: 'returned-id' })) 52 | const result = await sdk.send({ 53 | voice: { 54 | from: 'Notifme', 55 | to: '+15000000001', 56 | url: 'https://notifme.github.io', 57 | method: 'POST', 58 | fallbackUrl: 'http://example.com', 59 | fallbackMethod: 'POST', 60 | statusCallback: 'http://example.com', 61 | statusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'], 62 | sendDigits: 'ww1234', 63 | machineDetection: 'Enable', 64 | machineDetectionTimeout: 30, 65 | timeout: 60, 66 | customize: async (provider, request) => ({ ...request, url: 'url...' }) 67 | } 68 | }) 69 | expect(mockHttp).lastCalledWith(expect.objectContaining({ 70 | hostname: 'api.twilio.com', 71 | method: 'POST', 72 | path: '/2010-04-01/Accounts/account/Calls.json', 73 | protocol: 'https:', 74 | href: 'https://api.twilio.com/2010-04-01/Accounts/account/Calls.json', 75 | headers: expect.objectContaining({ 76 | Accept: ['*/*'], 77 | Authorization: ['Basic YWNjb3VudDp0b2tlbg=='], 78 | 'Content-Type': [expect.stringContaining('multipart/form-data;boundary=')], 79 | 'User-Agent': ['notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)'] 80 | }) 81 | })) 82 | expect(result).toEqual({ 83 | status: 'success', 84 | channels: { 85 | voice: { id: 'returned-id', providerId: 'voice-twilio-provider' } 86 | } 87 | }) 88 | }) 89 | 90 | test('Twilio API error.', async () => { 91 | mockResponse(400, JSON.stringify({ message: 'error!' })) 92 | const result = await sdk.send(request) 93 | expect(result).toEqual({ 94 | status: 'error', 95 | errors: { 96 | voice: '400 - error!' 97 | }, 98 | channels: { 99 | voice: { id: undefined, providerId: 'voice-twilio-provider' } 100 | } 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /__tests__/providers/webpush/gcm.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | import webpush from 'web-push' 5 | 6 | jest.mock('web-push') 7 | jest.mock('../../../src/util/logger', () => ({ 8 | warn: jest.fn() 9 | })) 10 | webpush.sendNotification.mockReturnValue({ headers: { location: 'returned-id' } }) 11 | 12 | const request = { 13 | webpush: { 14 | subscription: { 15 | keys: { 16 | auth: 'xxxxx', 17 | p256dh: 'xxxxx' 18 | }, 19 | endpoint: 'xxxxx' 20 | }, 21 | title: 'Hi John', 22 | body: 'Hello John! How are you?', 23 | icon: 'https://notifme.github.io/notifme-sdk/img/icon.png' 24 | } 25 | } 26 | 27 | test('GCM with API key.', async () => { 28 | const sdk = new NotifmeSdk({ 29 | channels: { 30 | webpush: { 31 | providers: [{ 32 | type: 'gcm', 33 | gcmAPIKey: 'xxxxx' 34 | }] 35 | } 36 | } 37 | }) 38 | const result = await sdk.send(request) 39 | expect(webpush.setGCMAPIKey).lastCalledWith('xxxxx') 40 | expect(webpush.sendNotification).lastCalledWith( 41 | request.webpush.subscription, 42 | '{"title":"Hi John","body":"Hello John! How are you?","icon":"https://notifme.github.io/notifme-sdk/img/icon.png"}', 43 | { TTL: undefined, headers: undefined } 44 | ) 45 | expect(result).toEqual({ 46 | status: 'success', 47 | channels: { 48 | webpush: { id: 'returned-id', providerId: 'webpush-gcm-provider' } 49 | } 50 | }) 51 | }) 52 | 53 | test('GCM with vapid.', async () => { 54 | const sdk = new NotifmeSdk({ 55 | channels: { 56 | webpush: { 57 | providers: [{ 58 | type: 'gcm', 59 | vapidDetails: { subject: 'xxxx', publicKey: 'xxxxx', privateKey: 'xxxxxx' } 60 | }] 61 | } 62 | } 63 | }) 64 | const result = await sdk.send(request) 65 | expect(webpush.setVapidDetails).lastCalledWith('xxxx', 'xxxxx', 'xxxxxx') 66 | expect(webpush.sendNotification).lastCalledWith( 67 | request.webpush.subscription, 68 | '{"title":"Hi John","body":"Hello John! How are you?","icon":"https://notifme.github.io/notifme-sdk/img/icon.png"}', 69 | { TTL: undefined, headers: undefined } 70 | ) 71 | expect(result).toEqual({ 72 | status: 'success', 73 | channels: { 74 | webpush: { id: 'returned-id', providerId: 'webpush-gcm-provider' } 75 | } 76 | }) 77 | }) 78 | 79 | test('GCM should customize requests.', async () => { 80 | const sdk = new NotifmeSdk({ 81 | channels: { 82 | webpush: { 83 | providers: [{ 84 | type: 'gcm', 85 | vapidDetails: { subject: 'xxxx', publicKey: 'xxxxx', privateKey: 'xxxxxx' } 86 | }] 87 | } 88 | } 89 | }) 90 | await sdk.send({ 91 | webpush: { 92 | ...request.webpush, 93 | customize: async (provider, request) => ({ ...request, title: 'Hi John!' }) 94 | } 95 | }) 96 | expect(webpush.sendNotification).lastCalledWith( 97 | request.webpush.subscription, 98 | '{"title":"Hi John!","body":"Hello John! How are you?","icon":"https://notifme.github.io/notifme-sdk/img/icon.png"}', 99 | { TTL: undefined, headers: undefined } 100 | ) 101 | }) 102 | -------------------------------------------------------------------------------- /__tests__/providers/webpush/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | jest.mock('../../../src/util/logger', () => ({ 6 | info: jest.fn(), 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | webpush: { 12 | subscription: { 13 | keys: { 14 | auth: 'xxxxx', 15 | p256dh: 'xxxxx' 16 | }, 17 | endpoint: 'xxxxx' 18 | }, 19 | title: 'Hi John', 20 | body: 'Hello John! How are you?', 21 | icon: 'https://notifme.github.io/notifme-sdk/img/icon.png' 22 | } 23 | } 24 | 25 | test('webpush unknown provider.', async () => { 26 | const sdk = new NotifmeSdk({ 27 | channels: { 28 | webpush: { 29 | providers: [{ 30 | type: 'custom', 31 | id: 'my-custom-webpush-provider', 32 | send: async () => 'custom-returned-id' 33 | }] 34 | } 35 | } 36 | }) 37 | const result = await sdk.send(request) 38 | expect(result).toEqual({ 39 | status: 'success', 40 | channels: { 41 | webpush: { id: 'custom-returned-id', providerId: 'my-custom-webpush-provider' } 42 | } 43 | }) 44 | }) 45 | 46 | test('webpush custom provider.', async () => { 47 | // $FlowIgnore 48 | expect(() => (new NotifmeSdk({ 49 | channels: { 50 | webpush: { 51 | providers: [{ 52 | type: 'unknown' 53 | }] 54 | } 55 | } 56 | }) 57 | )).toThrow('Unknown webpush provider "unknown".') 58 | }) 59 | 60 | test('webpush logger provider.', async () => { 61 | const sdk = new NotifmeSdk({ 62 | channels: { 63 | webpush: { 64 | providers: [{ 65 | type: 'logger' 66 | }] 67 | } 68 | } 69 | }) 70 | const result = await sdk.send(request) 71 | expect(result).toEqual({ 72 | status: 'success', 73 | channels: { 74 | webpush: { id: expect.stringContaining('id-'), providerId: 'webpush-logger-provider' } 75 | } 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /__tests__/providers/webpush/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSend = jest.fn() 6 | jest.mock('../../../src/providers/email/smtp', () => () => ({ 7 | send: mockSend 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | useNotificationCatcher: true 12 | }) 13 | 14 | const request = { 15 | webpush: { 16 | subscription: { 17 | keys: { 18 | auth: 'xxxxx', 19 | p256dh: 'xxxxx' 20 | }, 21 | endpoint: 'xxxxx' 22 | }, 23 | title: 'Hi John', 24 | body: 'Hello John! How are you?', 25 | icon: 'https://notifme.github.io/notifme-sdk/img/icon.png' 26 | } 27 | } 28 | 29 | test('webpush notification catcher provider should use SMTP provider.', async () => { 30 | const result = await sdk.send(request) 31 | expect(mockSend).lastCalledWith({ 32 | from: '-', 33 | headers: { 34 | 'X-payload': '{"title":"Hi John","body":"Hello John! How are you?","icon":"https://notifme.github.io/notifme-sdk/img/icon.png"}', 35 | 'X-to': '[webpush] ', 36 | 'X-type': 'webpush' 37 | }, 38 | subject: 'Hi John', 39 | to: 'user@webpush' 40 | }) 41 | expect(result).toEqual({ 42 | status: 'success', 43 | channels: { 44 | webpush: { id: undefined, providerId: 'webpush-notificationcatcher-provider' } 45 | } 46 | }) 47 | }) 48 | 49 | test('webpush notification catcher provider should use SMTP provider (with userId).', async () => { 50 | const result = await sdk.send({ metadata: { userId: '24' }, ...request }) 51 | expect(mockSend).lastCalledWith({ 52 | from: '-', 53 | headers: { 54 | 'X-payload': '{"title":"Hi John","userId":"24","body":"Hello John! How are you?","icon":"https://notifme.github.io/notifme-sdk/img/icon.png"}', 55 | 'X-to': '[webpush] 24', 56 | 'X-type': 'webpush' 57 | }, 58 | subject: 'Hi John', 59 | to: '24@webpush' 60 | }) 61 | expect(result).toEqual({ 62 | status: 'success', 63 | channels: { 64 | webpush: { id: undefined, providerId: 'webpush-notificationcatcher-provider' } 65 | } 66 | }) 67 | }) 68 | 69 | test('webpush notification catcher provider should customize requests.', async () => { 70 | await sdk.send({ 71 | metadata: { userId: '24' }, 72 | webpush: { 73 | ...request.webpush, 74 | customize: async (provider, request) => ({ ...request, title: 'Hi John!' }) 75 | } 76 | }) 77 | expect(mockSend).lastCalledWith({ 78 | from: '-', 79 | headers: { 80 | 'X-payload': '{"title":"Hi John!","userId":"24","body":"Hello John! How are you?","icon":"https://notifme.github.io/notifme-sdk/img/icon.png"}', 81 | 'X-to': '[webpush] 24', 82 | 'X-type': 'webpush' 83 | }, 84 | subject: 'Hi John!', 85 | to: '24@webpush' 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /__tests__/providers/whatsapp/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | jest.mock('../../../src/util/logger', () => ({ 6 | info: jest.fn(), 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | whatsapp: { 12 | from: 'me', 13 | to: 'you', 14 | type: 'text', 15 | text: 'Hello John! How are you?' 16 | } 17 | } 18 | 19 | test('whatsapp unknown provider.', async () => { 20 | const sdk = new NotifmeSdk({ 21 | channels: { 22 | whatsapp: { 23 | providers: [{ 24 | type: 'custom', 25 | id: 'my-custom-whatsapp-provider', 26 | send: async () => 'custom-returned-id' 27 | }] 28 | } 29 | } 30 | }) 31 | const result = await sdk.send(request) 32 | expect(result).toEqual({ 33 | status: 'success', 34 | channels: { 35 | whatsapp: { id: 'custom-returned-id', providerId: 'my-custom-whatsapp-provider' } 36 | } 37 | }) 38 | }) 39 | 40 | test('whatsapp custom provider.', async () => { 41 | // $FlowIgnore 42 | expect(() => (new NotifmeSdk({ 43 | channels: { 44 | whatsapp: { 45 | providers: [{ 46 | type: 'unknown' 47 | }] 48 | } 49 | } 50 | }) 51 | )).toThrow('Unknown whatsapp provider "unknown".') 52 | }) 53 | 54 | test('whatsapp logger provider.', async () => { 55 | const sdk = new NotifmeSdk({ 56 | channels: { 57 | whatsapp: { 58 | providers: [{ 59 | type: 'logger' 60 | }] 61 | } 62 | } 63 | }) 64 | const result = await sdk.send(request) 65 | expect(result).toEqual({ 66 | status: 'success', 67 | channels: { 68 | whatsapp: { id: expect.stringContaining('id-'), providerId: 'whatsapp-logger-provider' } 69 | } 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /__tests__/providers/whatsapp/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import NotifmeSdk from '../../../src' 4 | 5 | const mockSend = jest.fn() 6 | jest.mock('../../../src/providers/email/smtp', () => () => ({ 7 | send: mockSend 8 | })) 9 | 10 | const sdk = new NotifmeSdk({ 11 | useNotificationCatcher: true 12 | }) 13 | 14 | test('whatsapp notification catcher provider should use SMTP provider.', async () => { 15 | const result = await sdk.send({ 16 | whatsapp: { 17 | from: 'me', 18 | to: 'you', 19 | type: 'text', 20 | text: 'Hello John!' 21 | } 22 | }) 23 | expect(mockSend).lastCalledWith({ 24 | to: 'you@whatsapp', 25 | from: 'me', 26 | subject: 'Hello John!', 27 | text: JSON.stringify({ text: 'Hello John!', type: 'text' }, null, 2), 28 | headers: { 29 | 'X-type': 'whatsapp', 30 | 'X-to': '[whatsapp] you' 31 | } 32 | }) 33 | expect(result).toEqual({ 34 | status: 'success', 35 | channels: { 36 | whatsapp: { id: '', providerId: 'whatsapp-notificationcatcher-provider' } 37 | } 38 | }) 39 | }) 40 | 41 | test('whatsapp notification catcher provider should use SMTP provider (template).', async () => { 42 | const result = await sdk.send({ 43 | whatsapp: { 44 | from: 'me', 45 | to: 'you', 46 | type: 'template', 47 | templateName: 'template-name', 48 | templateData: { body: { placeholders: ['John'] } } 49 | } 50 | }) 51 | expect(mockSend).lastCalledWith({ 52 | to: 'you@whatsapp', 53 | from: 'me', 54 | subject: '', 55 | text: JSON.stringify({ type: 'template', templateName: 'template-name', templateData: { body: { placeholders: ['John'] } } }, null, 2), 56 | headers: { 57 | 'X-type': 'whatsapp', 58 | 'X-to': '[whatsapp] you' 59 | } 60 | }) 61 | expect(result).toEqual({ 62 | status: 'success', 63 | channels: { 64 | whatsapp: { id: '', providerId: 'whatsapp-notificationcatcher-provider' } 65 | } 66 | }) 67 | }) 68 | 69 | test('whatsapp notification catcher provider should use SMTP provider (long message).', async () => { 70 | const result = await sdk.send({ 71 | whatsapp: { 72 | from: 'me', 73 | to: 'you', 74 | type: 'text', 75 | text: 'Hello John! How are you?' 76 | } 77 | }) 78 | expect(mockSend).lastCalledWith({ 79 | to: 'you@whatsapp', 80 | from: 'me', 81 | subject: 'Hello John! How are ...', 82 | text: JSON.stringify({ text: 'Hello John! How are you?', type: 'text' }, null, 2), 83 | headers: { 84 | 'X-type': 'whatsapp', 85 | 'X-to': '[whatsapp] you' 86 | } 87 | }) 88 | expect(result).toEqual({ 89 | status: 'success', 90 | channels: { 91 | whatsapp: { id: '', providerId: 'whatsapp-notificationcatcher-provider' } 92 | } 93 | }) 94 | }) 95 | 96 | test('whatsapp customized success.', async () => { 97 | await sdk.send({ 98 | whatsapp: { 99 | from: 'me', 100 | to: 'you', 101 | type: 'text', 102 | text: '', 103 | customize: async (provider, request) => ({ 104 | from: 'me', 105 | to: 'you', 106 | type: 'text', 107 | text: 'Hello John! How are you?' 108 | }) 109 | } 110 | }) 111 | expect(mockSend).lastCalledWith({ 112 | to: 'you@whatsapp', 113 | from: 'me', 114 | subject: 'Hello John! How are ...', 115 | text: JSON.stringify({ text: 'Hello John! How are you?', type: 'text' }, null, 2), 116 | headers: { 117 | 'X-type': 'whatsapp', 118 | 'X-to': '[whatsapp] you' 119 | } 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /__tests__/sender.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import Sender from '../src/sender' 4 | import strategyNoFallback from '../src/strategies/providers/no-fallback' 5 | import logger from '../src/util/logger' 6 | 7 | jest.mock('../src/util/logger', () => ({ 8 | info: jest.fn(), 9 | warn: jest.fn() 10 | })) 11 | 12 | const providers = { 13 | email: [{ id: 'email-provider', send: jest.fn() }], 14 | sms: [{ id: 'sms-provider', send: async () => '24' }], 15 | voice: [{ id: 'voice-provider', send: async () => '24' }], 16 | push: [], // no push provider 17 | webpush: [{ id: 'webpush-provider', send: () => { throw new Error('webpush test error') } }] 18 | } 19 | const strategies = { 20 | email: strategyNoFallback, 21 | sms: strategyNoFallback, 22 | voice: strategyNoFallback, 23 | push: strategyNoFallback, 24 | webpush: strategyNoFallback 25 | } 26 | 27 | const sender = new Sender(['email', 'sms', 'voice', 'push', 'webpush'], providers, strategies) 28 | 29 | test('Sender should send all notifications.', async () => { 30 | const metadata = { id: '24' } 31 | const request = { 32 | metadata, 33 | email: { from: 'me@example.com', to: 'john@example.com', subject: 'Hi John', html: 'Hello John! How are you?' }, 34 | sms: { from: '+15000000000', to: '+15000000001', text: 'Hello John! How are you?' }, 35 | voice: { from: '+15000000000', to: '+15000000001', url: 'https://notifme.github.io' }, 36 | push: { registrationToken: 'xxxxx', title: 'Hi John', body: 'Hello John! How are you?' }, 37 | webpush: { subscription: { keys: { auth: 'xxxxx', p256dh: 'xxxxx' }, endpoint: 'xxxxx' }, title: 'Hi John', body: 'Hello John! How are you?' } 38 | } 39 | 40 | const result = await sender.send(request) 41 | expect(providers.email[0].send).toBeCalledWith({ ...metadata, ...request.email }) 42 | expect(logger.warn).toBeCalledWith('No provider registered for channel "push". Using logger.') 43 | expect(logger.info).toBeCalledWith('[PUSH] Sent by "push-logger-provider":') 44 | expect(logger.info).toBeCalledWith({ id: '24', registrationToken: 'xxxxx', title: 'Hi John', body: 'Hello John! How are you?' }) 45 | expect(logger.warn).toBeCalledWith('webpush-provider', new Error('webpush test error')) 46 | expect(result).toEqual({ 47 | status: 'error', 48 | channels: { 49 | email: { id: undefined, providerId: 'email-provider' }, 50 | sms: { id: '24', providerId: 'sms-provider' }, 51 | voice: { id: '24', providerId: 'voice-provider' }, 52 | push: { id: (result.channels: any).push.id, providerId: 'push-logger-provider' }, 53 | webpush: { id: undefined, providerId: 'webpush-provider' } 54 | }, 55 | errors: { 56 | webpush: 'webpush test error' 57 | } 58 | }) 59 | 60 | expect(await sender.send({ sms: request.sms })).toEqual({ 61 | status: 'success', 62 | channels: { 63 | sms: { id: '24', providerId: 'sms-provider' } 64 | } 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /__tests__/strategies/providers/fallback.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import strategyFallback from '../../../src/strategies/providers/fallback' 4 | import logger from '../../../src/util/logger' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const failingProviders = [ 11 | { 12 | id: 'sms-provider-1', 13 | send: async () => { throw new Error('error provider 1') } 14 | }, { 15 | id: 'sms-provider-2', 16 | send: async () => { throw new Error('error provider 2') } 17 | } 18 | ] 19 | 20 | test('Fallback strategy should call all providers and return success if one succeeded.', async () => { 21 | const strategy = strategyFallback([ 22 | ...failingProviders, 23 | { 24 | id: 'sms-provider-3', 25 | send: async () => '24' 26 | } 27 | ]) 28 | const result = await strategy({ 29 | sms: { from: '+15000000000', to: '+15000000001', text: 'Hello John! How are you?' } 30 | }) 31 | 32 | expect(logger.warn).toBeCalledWith('sms-provider-1', new Error('error provider 1')) 33 | expect(logger.warn).toBeCalledWith('sms-provider-2', new Error('error provider 2')) 34 | expect(result).toEqual({ providerId: 'sms-provider-3', id: '24' }) 35 | }) 36 | 37 | test('Fallback strategy should call all providers and throw error if all failed.', async () => { 38 | const strategy = strategyFallback(failingProviders) 39 | 40 | let error 41 | try { 42 | await strategy({ 43 | sms: { from: '+15000000000', to: '+15000000001', text: 'Hello John! How are you?' } 44 | }) 45 | } catch (e) { 46 | error = e 47 | } 48 | expect(logger.warn).toBeCalledWith('sms-provider-1', new Error('error provider 1')) 49 | expect(logger.warn).toBeCalledWith('sms-provider-2', new Error('error provider 2')) 50 | expect(error).toEqual(new Error('error provider 2')) 51 | }) 52 | -------------------------------------------------------------------------------- /__tests__/strategies/providers/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global test, expect */ 3 | import strategyProvidersFactory from '../../../src/strategies/providers' 4 | import strategyFallback from '../../../src/strategies/providers/fallback' 5 | import strategyNoFallback from '../../../src/strategies/providers/no-fallback' 6 | import strategyRoundrobin from '../../../src/strategies/providers/roundrobin' 7 | 8 | test('Strategy provider factory should replace provider key with its corresponding function.', () => { 9 | const customStrategy = () => {} 10 | expect(strategyProvidersFactory({ 11 | email: { 12 | providers: [], 13 | multiProviderStrategy: 'no-fallback' 14 | }, 15 | sms: { 16 | providers: [], 17 | multiProviderStrategy: 'fallback' 18 | }, 19 | voice: { 20 | providers: [], 21 | multiProviderStrategy: 'fallback' 22 | }, 23 | push: { 24 | providers: [], 25 | multiProviderStrategy: 'roundrobin' 26 | }, 27 | webpush: { 28 | providers: [], 29 | multiProviderStrategy: customStrategy 30 | } 31 | })).toEqual({ 32 | email: strategyNoFallback, 33 | sms: strategyFallback, 34 | voice: strategyFallback, 35 | push: strategyRoundrobin, 36 | webpush: customStrategy 37 | }) 38 | }) 39 | 40 | test('Strategy provider factory should throw an error if the strategy does not exist.', () => { 41 | expect(() => { 42 | strategyProvidersFactory({ 43 | email: { 44 | providers: [], 45 | multiProviderStrategy: 'unknown-strategy' 46 | } 47 | }) 48 | }).toThrow('"unknown-strategy" is not a valid strategy. Strategy must be a function or fallback|no-fallback|roundrobin.') 49 | }) 50 | -------------------------------------------------------------------------------- /__tests__/strategies/providers/no-fallback.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import strategyNoFallback from '../../../src/strategies/providers/no-fallback' 4 | import logger from '../../../src/util/logger' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | test('No-Fallback strategy should call first provider and return success if it succeeded.', async () => { 11 | const strategy = strategyNoFallback([ 12 | { 13 | id: 'sms-provider-1', 14 | send: async () => '24' 15 | } 16 | ]) 17 | const result = await strategy({ 18 | sms: { from: '+15000000000', to: '+15000000001', text: 'Hello John! How are you?' } 19 | }) 20 | 21 | expect(result).toEqual({ providerId: 'sms-provider-1', id: '24' }) 22 | }) 23 | 24 | test('No-Fallback strategy should call first provider and throw error if it failed.', async () => { 25 | const strategy = strategyNoFallback([ 26 | { 27 | id: 'sms-provider-1', 28 | send: async () => { throw new Error('error provider 1') } 29 | }, { 30 | id: 'sms-provider-2', 31 | send: async () => '24' 32 | } 33 | ]) 34 | 35 | let error 36 | try { 37 | await strategy({ 38 | sms: { from: '+15000000000', to: '+15000000001', text: 'Hello John! How are you?' } 39 | }) 40 | } catch (e) { 41 | error = e 42 | } 43 | expect(logger.warn).toBeCalledWith('sms-provider-1', new Error('error provider 1')) 44 | expect(error).toEqual(new Error('error provider 1')) 45 | }) 46 | -------------------------------------------------------------------------------- /__tests__/strategies/providers/roundrobin.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* global jest, test, expect */ 3 | import strategyRoundrobin from '../../../src/strategies/providers/roundrobin' 4 | import logger from '../../../src/util/logger' 5 | 6 | jest.mock('../../../src/util/logger', () => ({ 7 | warn: jest.fn() 8 | })) 9 | 10 | const request = { 11 | sms: { from: '+15000000000', to: '+15000000001', text: 'Hello John! How are you?' } 12 | } 13 | 14 | test('Roundrobin strategy should call all providers in turns.', async () => { 15 | const strategy = strategyRoundrobin([ 16 | { 17 | id: 'sms-provider-1', 18 | send: async () => '24' 19 | }, { 20 | id: 'sms-provider-2', 21 | send: async () => '24' 22 | }, { 23 | id: 'sms-provider-3', 24 | send: async () => { throw new Error('error provider 3') } 25 | }, { 26 | id: 'sms-provider-4', 27 | send: async () => '24' 28 | } 29 | ]) 30 | 31 | // First call 32 | expect(await strategy(request)).toEqual({ providerId: 'sms-provider-1', id: '24' }) 33 | // Second call 34 | expect(await strategy(request)).toEqual({ providerId: 'sms-provider-2', id: '24' }) 35 | // Third call 36 | expect(await strategy(request)).toEqual({ providerId: 'sms-provider-4', id: '24' }) 37 | expect(logger.warn).toBeCalledWith('sms-provider-3', new Error('error provider 3')) 38 | // Fourth call 39 | expect(await strategy(request)).toEqual({ providerId: 'sms-provider-4', id: '24' }) 40 | // Fifth call 41 | expect(await strategy(request)).toEqual({ providerId: 'sms-provider-1', id: '24' }) 42 | // Sixth call 43 | expect(await strategy(request)).toEqual({ providerId: 'sms-provider-2', id: '24' }) 44 | // Seventh call 45 | expect(await strategy(request)).toEqual({ providerId: 'sms-provider-4', id: '24' }) 46 | expect(logger.warn).toBeCalledWith('sms-provider-3', new Error('error provider 3')) 47 | }) 48 | 49 | test('Roundrobin strategy should throw an error if all providers failed.', async () => { 50 | const strategy = strategyRoundrobin([ 51 | { 52 | id: 'sms-provider-1', 53 | send: async () => { throw new Error('error provider 1') } 54 | }, { 55 | id: 'sms-provider-2', 56 | send: async () => { throw new Error('error provider 2') } 57 | }, { 58 | id: 'sms-provider-3', 59 | send: async () => { throw new Error('error provider 3') } 60 | } 61 | ]) 62 | 63 | let error 64 | try { 65 | await strategy(request) 66 | } catch (e) { 67 | error = e 68 | } 69 | expect(logger.warn).toBeCalledWith('sms-provider-1', new Error('error provider 1')) 70 | expect(logger.warn).toBeCalledWith('sms-provider-2', new Error('error provider 2')) 71 | expect(logger.warn).toBeCalledWith('sms-provider-3', new Error('error provider 3')) 72 | expect(error).toEqual(new Error('error provider 3')) 73 | }) 74 | -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notifme/notifme-sdk/436b785b1dc12e040a86085a0d4582408804dd63/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/getting-started-sms-catcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notifme/notifme-sdk/436b785b1dc12e040a86085a0d4582408804dd63/docs/img/getting-started-sms-catcher.png -------------------------------------------------------------------------------- /docs/img/getting-started-sms-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notifme/notifme-sdk/436b785b1dc12e040a86085a0d4582408804dd63/docs/img/getting-started-sms-log.png -------------------------------------------------------------------------------- /docs/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notifme/notifme-sdk/436b785b1dc12e040a86085a0d4582408804dd63/docs/img/icon.png -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notifme/notifme-sdk/436b785b1dc12e040a86085a0d4582408804dd63/docs/img/logo.png -------------------------------------------------------------------------------- /examples/email.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | const NotifmeSdk = require('../src').default // notifme-sdk 3 | 4 | const notifmeSdk = new NotifmeSdk({}) 5 | 6 | const notificationRequest = { 7 | email: { 8 | from: 'from@example.com', 9 | to: 'to@example.com', 10 | subject: 'Hi John', 11 | html: 'Hello John! How are you?', 12 | replyTo: 'replyto@example.com', 13 | text: 'Hello John! How are you?', 14 | headers: { 'My-Custom-Header': 'my-value' }, 15 | cc: ['cc1@example.com', 'cc2@example.com'], 16 | bcc: ['bcc@example.com'], 17 | attachments: [{ 18 | contentType: 'text/plain', 19 | filename: 'test.txt', 20 | content: 'hello!' 21 | }] 22 | } 23 | } 24 | 25 | notifmeSdk.send(notificationRequest).then(console.log) 26 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | const NotifmeSdk = require('../src').default // notifme-sdk 3 | 4 | const notifmeSdk = new NotifmeSdk({}) 5 | 6 | const notificationRequest = { 7 | metadata: { 8 | id: '24' 9 | }, 10 | email: { 11 | from: 'me@example.com', 12 | to: 'john@example.com', 13 | subject: 'Hi John', 14 | html: 'Hello John! How are you?' 15 | }, 16 | sms: { 17 | from: '+15000000000', 18 | to: '+15000000001', 19 | text: 'Hello John! How are you?' 20 | }, 21 | push: { 22 | registrationToken: 'xxxxx', 23 | title: 'Hi John', 24 | body: 'Hello John! How are you?', 25 | icon: 'https://notifme.github.io/notifme-sdk/img/icon.png' 26 | }, 27 | webpush: { 28 | subscription: { 29 | keys: { 30 | auth: 'xxxxx', 31 | p256dh: 'xxxxx' 32 | }, 33 | endpoint: 'xxxxx' 34 | }, 35 | title: 'Hi John', 36 | body: 'Hello John! How are you?', 37 | icon: 'https://notifme.github.io/notifme-sdk/img/icon.png' 38 | } 39 | } 40 | 41 | notifmeSdk.send(notificationRequest).then(console.log) 42 | -------------------------------------------------------------------------------- /examples/slack.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | const NotifmeSdk = require('../src').default // notifme-sdk 3 | 4 | const notifmeSdk = new NotifmeSdk({ 5 | channels: { 6 | slack: { 7 | providers: [{ 8 | type: 'logger' 9 | }] 10 | } 11 | } 12 | }) 13 | 14 | const notificationRequest = { 15 | metadata: { 16 | id: '24' 17 | }, 18 | slack: { 19 | text: 'Test Slack webhook :)' 20 | } 21 | } 22 | 23 | notifmeSdk.send(notificationRequest).then(console.log) 24 | -------------------------------------------------------------------------------- /examples/with-notification-catcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotifmeSdk from '../src' // notifme-sdk 3 | // Types 4 | import type { NotificationRequestType } from '../src' // notifme-sdk 5 | 6 | /* 7 | * Note: Notification catcher must be running locally. 8 | * Run `yarn add --dev notification-catcher && yarn run notification-catcher` 9 | */ 10 | 11 | const notifmeSdk = new NotifmeSdk({ 12 | useNotificationCatcher: true 13 | }) 14 | 15 | const notificationRequest: NotificationRequestType = { 16 | email: { 17 | from: 'me@example.com', 18 | to: 'john@example.com', 19 | subject: 'Hi John', 20 | html: 'Hello John! How are you?' 21 | }, 22 | sms: { 23 | from: '+15000000000', 24 | to: '+15000000001', 25 | text: 'Hello John! How are you?' 26 | }, 27 | push: { 28 | registrationToken: 'xxxxx', 29 | title: 'Hi John', 30 | body: 'Hello John! How are you?', 31 | icon: 'https://notifme.github.io/notifme-sdk/img/icon.png' 32 | }, 33 | webpush: { 34 | subscription: { 35 | keys: { 36 | auth: 'xxxxx', 37 | p256dh: 'xxxxx' 38 | }, 39 | endpoint: 'xxxxx' 40 | }, 41 | title: 'Hi John', 42 | body: 'Hello John! How are you?', 43 | icon: 'https://notifme.github.io/notifme-sdk/img/icon.png' 44 | } 45 | } 46 | 47 | const run = async () => { 48 | console.log(await notifmeSdk.send(notificationRequest)) 49 | } 50 | run() 51 | -------------------------------------------------------------------------------- /lib/models/notification-request.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/models/provider-email.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/models/provider-push.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/models/provider-slack.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/models/provider-sms.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/models/provider-voice.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/models/provider-webpush.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/models/provider-whatsapp.js: -------------------------------------------------------------------------------- 1 | "use strict"; -------------------------------------------------------------------------------- /lib/providers/email/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 10 | var _logger = _interopRequireDefault(require("../logger")); 11 | var _mailgun = _interopRequireDefault(require("./mailgun")); 12 | var _mandrill = _interopRequireDefault(require("./mandrill")); 13 | var _notificationCatcher = _interopRequireDefault(require("./notificationCatcher")); 14 | var _sendgrid = _interopRequireDefault(require("./sendgrid")); 15 | var _ses = _interopRequireDefault(require("./ses")); 16 | var _sendmail = _interopRequireDefault(require("./sendmail")); 17 | var _smtp = _interopRequireDefault(require("./smtp")); 18 | var _sparkpost = _interopRequireDefault(require("./sparkpost")); 19 | var _excluded = ["type"]; 20 | // Types 21 | 22 | function factory(_ref) { 23 | var type = _ref.type, 24 | config = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 25 | switch (type) { 26 | // Development 27 | case 'logger': 28 | return new _logger["default"](config, 'email'); 29 | case 'notificationcatcher': 30 | return new _notificationCatcher["default"]('email'); 31 | 32 | // Custom 33 | case 'custom': 34 | return config; 35 | 36 | // Protocols 37 | case 'sendmail': 38 | return new _sendmail["default"](config); 39 | case 'smtp': 40 | return new _smtp["default"](config); 41 | 42 | // Providers 43 | case 'mailgun': 44 | return new _mailgun["default"](config); 45 | case 'mandrill': 46 | return new _mandrill["default"](config); 47 | case 'sendgrid': 48 | return new _sendgrid["default"](config); 49 | case 'ses': 50 | return new _ses["default"](config); 51 | case 'sparkpost': 52 | return new _sparkpost["default"](config); 53 | default: 54 | throw new Error("Unknown email provider \"".concat(type, "\".")); 55 | } 56 | } -------------------------------------------------------------------------------- /lib/providers/email/sendmail.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 11 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 13 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 14 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); 15 | var _nodemailer = _interopRequireDefault(require("nodemailer")); 16 | var _excluded = ["customize"]; 17 | // Types 18 | var EmailSendmailProvider = exports["default"] = /*#__PURE__*/function () { 19 | function EmailSendmailProvider(config) { 20 | (0, _classCallCheck2["default"])(this, EmailSendmailProvider); 21 | (0, _defineProperty2["default"])(this, "id", 'email-sendmail-provider'); 22 | this.transporter = _nodemailer["default"].createTransport(config); 23 | } 24 | return (0, _createClass2["default"])(EmailSendmailProvider, [{ 25 | key: "send", 26 | value: function () { 27 | var _send = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 28 | var _ref, customize, rest, result; 29 | return _regenerator["default"].wrap(function _callee$(_context) { 30 | while (1) switch (_context.prev = _context.next) { 31 | case 0: 32 | if (!request.customize) { 33 | _context.next = 6; 34 | break; 35 | } 36 | _context.next = 3; 37 | return request.customize(this.id, request); 38 | case 3: 39 | _context.t0 = _context.sent; 40 | _context.next = 7; 41 | break; 42 | case 6: 43 | _context.t0 = request; 44 | case 7: 45 | _ref = _context.t0; 46 | customize = _ref.customize; 47 | rest = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 48 | _context.next = 12; 49 | return this.transporter.sendMail(rest); 50 | case 12: 51 | result = _context.sent; 52 | return _context.abrupt("return", result.messageId); 53 | case 14: 54 | case "end": 55 | return _context.stop(); 56 | } 57 | }, _callee, this); 58 | })); 59 | function send(_x) { 60 | return _send.apply(this, arguments); 61 | } 62 | return send; 63 | }() 64 | }]); 65 | }(); -------------------------------------------------------------------------------- /lib/providers/email/smtp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 11 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 13 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 14 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); 15 | var _nodemailer = _interopRequireDefault(require("nodemailer")); 16 | var _excluded = ["customize"]; 17 | // Types 18 | var EmailSmtpProvider = exports["default"] = /*#__PURE__*/function () { 19 | function EmailSmtpProvider(config) { 20 | (0, _classCallCheck2["default"])(this, EmailSmtpProvider); 21 | (0, _defineProperty2["default"])(this, "id", 'email-smtp-provider'); 22 | this.transporter = _nodemailer["default"].createTransport(config); 23 | } 24 | return (0, _createClass2["default"])(EmailSmtpProvider, [{ 25 | key: "send", 26 | value: function () { 27 | var _send = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 28 | var _ref, customize, rest, result; 29 | return _regenerator["default"].wrap(function _callee$(_context) { 30 | while (1) switch (_context.prev = _context.next) { 31 | case 0: 32 | if (!request.customize) { 33 | _context.next = 6; 34 | break; 35 | } 36 | _context.next = 3; 37 | return request.customize(this.id, request); 38 | case 3: 39 | _context.t0 = _context.sent; 40 | _context.next = 7; 41 | break; 42 | case 6: 43 | _context.t0 = request; 44 | case 7: 45 | _ref = _context.t0; 46 | customize = _ref.customize; 47 | rest = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 48 | _context.next = 12; 49 | return this.transporter.sendMail(rest); 50 | case 12: 51 | result = _context.sent; 52 | return _context.abrupt("return", result.messageId); 53 | case 14: 54 | case "end": 55 | return _context.stop(); 56 | } 57 | }, _callee, this); 58 | })); 59 | function send(_x) { 60 | return _send.apply(this, arguments); 61 | } 62 | return send; 63 | }() 64 | }]); 65 | }(); -------------------------------------------------------------------------------- /lib/providers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _keys = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/keys")); 10 | var _email = _interopRequireDefault(require("./email")); 11 | var _push = _interopRequireDefault(require("./push")); 12 | var _sms = _interopRequireDefault(require("./sms")); 13 | var _voice = _interopRequireDefault(require("./voice")); 14 | var _webpush = _interopRequireDefault(require("./webpush")); 15 | var _slack = _interopRequireDefault(require("./slack")); 16 | var _whatsapp = _interopRequireDefault(require("./whatsapp")); 17 | // Types 18 | function factory(channels) { 19 | return (0, _keys["default"])(channels).reduce(function (acc, key) { 20 | acc[key] = channels[key].providers.map(function (config) { 21 | switch (key) { 22 | case 'email': 23 | return (0, _email["default"])(config); 24 | case 'sms': 25 | return (0, _sms["default"])(config); 26 | case 'voice': 27 | return (0, _voice["default"])(config); 28 | case 'push': 29 | return (0, _push["default"])(config); 30 | case 'webpush': 31 | return (0, _webpush["default"])(config); 32 | case 'slack': 33 | return (0, _slack["default"])(config); 34 | case 'whatsapp': 35 | return (0, _whatsapp["default"])(config); 36 | default: 37 | return config; 38 | } 39 | }); 40 | return acc; 41 | }, {}); 42 | } -------------------------------------------------------------------------------- /lib/providers/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 11 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 12 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 13 | var _logger = _interopRequireDefault(require("../util/logger")); 14 | // Types 15 | var LoggerProvider = exports["default"] = /*#__PURE__*/function () { 16 | function LoggerProvider(config, channel) { 17 | (0, _classCallCheck2["default"])(this, LoggerProvider); 18 | this.id = "".concat(channel, "-logger-provider"); 19 | this.channel = channel; 20 | } 21 | return (0, _createClass2["default"])(LoggerProvider, [{ 22 | key: "send", 23 | value: function () { 24 | var _send = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 25 | return _regenerator["default"].wrap(function _callee$(_context) { 26 | while (1) switch (_context.prev = _context.next) { 27 | case 0: 28 | _logger["default"].info("[".concat(this.channel.toUpperCase(), "] Sent by \"").concat(this.id, "\":")); 29 | _logger["default"].info(request); 30 | return _context.abrupt("return", "id-".concat(Math.round(Math.random() * 1000000000))); 31 | case 3: 32 | case "end": 33 | return _context.stop(); 34 | } 35 | }, _callee, this); 36 | })); 37 | function send(_x) { 38 | return _send.apply(this, arguments); 39 | } 40 | return send; 41 | }() 42 | }]); 43 | }(); -------------------------------------------------------------------------------- /lib/providers/push/apn.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 11 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 13 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 14 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); 15 | var _nodePushnotifications = _interopRequireDefault(require("node-pushnotifications")); 16 | var _excluded = ["registrationToken"]; 17 | // Types 18 | var PushApnProvider = exports["default"] = /*#__PURE__*/function () { 19 | function PushApnProvider(config) { 20 | (0, _classCallCheck2["default"])(this, PushApnProvider); 21 | (0, _defineProperty2["default"])(this, "id", 'push-apn-provider'); 22 | this.transporter = new _nodePushnotifications["default"]({ 23 | apn: config 24 | }); 25 | } 26 | return (0, _createClass2["default"])(PushApnProvider, [{ 27 | key: "send", 28 | value: function () { 29 | var _send = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 30 | var _ref, registrationToken, rest, result; 31 | return _regenerator["default"].wrap(function _callee$(_context) { 32 | while (1) switch (_context.prev = _context.next) { 33 | case 0: 34 | if (!request.customize) { 35 | _context.next = 6; 36 | break; 37 | } 38 | _context.next = 3; 39 | return request.customize(this.id, request); 40 | case 3: 41 | _context.t0 = _context.sent; 42 | _context.next = 7; 43 | break; 44 | case 6: 45 | _context.t0 = request; 46 | case 7: 47 | _ref = _context.t0; 48 | registrationToken = _ref.registrationToken; 49 | rest = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 50 | _context.next = 12; 51 | return this.transporter.send([registrationToken], rest); 52 | case 12: 53 | result = _context.sent; 54 | if (!(result[0].failure > 0)) { 55 | _context.next = 17; 56 | break; 57 | } 58 | throw new Error(result[0].message[0].error); 59 | case 17: 60 | return _context.abrupt("return", result[0].message[0].messageId); 61 | case 18: 62 | case "end": 63 | return _context.stop(); 64 | } 65 | }, _callee, this); 66 | })); 67 | function send(_x) { 68 | return _send.apply(this, arguments); 69 | } 70 | return send; 71 | }() 72 | }]); 73 | }(); -------------------------------------------------------------------------------- /lib/providers/push/fcm.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 11 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 13 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 14 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); 15 | var _nodePushnotifications = _interopRequireDefault(require("node-pushnotifications")); 16 | var _excluded = ["registrationToken"]; 17 | // Types 18 | var PushFcmProvider = exports["default"] = /*#__PURE__*/function () { 19 | function PushFcmProvider(config) { 20 | (0, _classCallCheck2["default"])(this, PushFcmProvider); 21 | (0, _defineProperty2["default"])(this, "id", 'push-fcm-provider'); 22 | this.transporter = new _nodePushnotifications["default"]({ 23 | gcm: config 24 | }); 25 | } 26 | return (0, _createClass2["default"])(PushFcmProvider, [{ 27 | key: "send", 28 | value: function () { 29 | var _send = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 30 | var _ref, registrationToken, rest, result; 31 | return _regenerator["default"].wrap(function _callee$(_context) { 32 | while (1) switch (_context.prev = _context.next) { 33 | case 0: 34 | if (!request.customize) { 35 | _context.next = 6; 36 | break; 37 | } 38 | _context.next = 3; 39 | return request.customize(this.id, request); 40 | case 3: 41 | _context.t0 = _context.sent; 42 | _context.next = 7; 43 | break; 44 | case 6: 45 | _context.t0 = request; 46 | case 7: 47 | _ref = _context.t0; 48 | registrationToken = _ref.registrationToken; 49 | rest = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 50 | _context.next = 12; 51 | return this.transporter.send([registrationToken], rest); 52 | case 12: 53 | result = _context.sent; 54 | if (!(result[0].failure > 0)) { 55 | _context.next = 17; 56 | break; 57 | } 58 | throw new Error(result[0].message[0].error); 59 | case 17: 60 | return _context.abrupt("return", result[0].message[0].messageId); 61 | case 18: 62 | case "end": 63 | return _context.stop(); 64 | } 65 | }, _callee, this); 66 | })); 67 | function send(_x) { 68 | return _send.apply(this, arguments); 69 | } 70 | return send; 71 | }() 72 | }]); 73 | }(); -------------------------------------------------------------------------------- /lib/providers/push/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 10 | var _adm = _interopRequireDefault(require("./adm")); 11 | var _apn = _interopRequireDefault(require("./apn")); 12 | var _fcm = _interopRequireDefault(require("./fcm")); 13 | var _logger = _interopRequireDefault(require("../logger")); 14 | var _notificationCatcher = _interopRequireDefault(require("./notificationCatcher")); 15 | var _wns = _interopRequireDefault(require("./wns")); 16 | var _excluded = ["type"]; 17 | // Types 18 | 19 | function factory(_ref) { 20 | var type = _ref.type, 21 | config = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 22 | switch (type) { 23 | // Development 24 | case 'logger': 25 | return new _logger["default"](config, 'push'); 26 | case 'notificationcatcher': 27 | return new _notificationCatcher["default"]('push'); 28 | 29 | // Custom 30 | case 'custom': 31 | return config; 32 | 33 | // Providers 34 | case 'adm': 35 | return new _adm["default"](config); 36 | case 'apn': 37 | return new _apn["default"](config); 38 | case 'fcm': 39 | return new _fcm["default"](config); 40 | case 'wns': 41 | return new _wns["default"](config); 42 | default: 43 | throw new Error("Unknown push provider \"".concat(type, "\".")); 44 | } 45 | } -------------------------------------------------------------------------------- /lib/providers/slack/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 10 | var _slack = _interopRequireDefault(require("./slack")); 11 | var _logger = _interopRequireDefault(require("../logger")); 12 | var _notificationCatcher = _interopRequireDefault(require("./notificationCatcher")); 13 | var _excluded = ["type"]; 14 | // Types 15 | 16 | function factory(_ref) { 17 | var type = _ref.type, 18 | config = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 19 | switch (type) { 20 | // Development 21 | case 'logger': 22 | return new _logger["default"](config, 'slack'); 23 | case 'notificationcatcher': 24 | return new _notificationCatcher["default"]('slack'); 25 | 26 | // Custom 27 | case 'custom': 28 | return config; 29 | 30 | // Providers 31 | case 'webhook': 32 | return new _slack["default"](config); 33 | default: 34 | throw new Error("Unknown slack provider \"".concat(type, "\".")); 35 | } 36 | } -------------------------------------------------------------------------------- /lib/providers/slack/slack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify")); 11 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 13 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 14 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 15 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); 16 | var _request = _interopRequireDefault(require("../../util/request")); 17 | var _excluded = ["webhookUrl"]; 18 | // Types 19 | var SlackProvider = exports["default"] = /*#__PURE__*/function () { 20 | function SlackProvider(config) { 21 | (0, _classCallCheck2["default"])(this, SlackProvider); 22 | (0, _defineProperty2["default"])(this, "id", 'slack-provider'); 23 | this.webhookUrl = config.webhookUrl; 24 | } 25 | return (0, _createClass2["default"])(SlackProvider, [{ 26 | key: "send", 27 | value: function () { 28 | var _send = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 29 | var _ref, webhookUrl, rest, apiRequest, response, responseText; 30 | return _regenerator["default"].wrap(function _callee$(_context) { 31 | while (1) switch (_context.prev = _context.next) { 32 | case 0: 33 | if (!request.customize) { 34 | _context.next = 6; 35 | break; 36 | } 37 | _context.next = 3; 38 | return request.customize(this.id, request); 39 | case 3: 40 | _context.t0 = _context.sent; 41 | _context.next = 7; 42 | break; 43 | case 6: 44 | _context.t0 = request; 45 | case 7: 46 | _ref = _context.t0; 47 | webhookUrl = _ref.webhookUrl; 48 | rest = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 49 | apiRequest = { 50 | method: 'POST', 51 | body: (0, _stringify["default"])(rest) 52 | }; 53 | _context.next = 13; 54 | return (0, _request["default"])(webhookUrl || this.webhookUrl, apiRequest); 55 | case 13: 56 | response = _context.sent; 57 | if (!response.ok) { 58 | _context.next = 18; 59 | break; 60 | } 61 | return _context.abrupt("return", ''); 62 | case 18: 63 | _context.next = 20; 64 | return response.text(); 65 | case 20: 66 | responseText = _context.sent; 67 | throw new Error("".concat(response.status, " - ").concat(responseText)); 68 | case 22: 69 | case "end": 70 | return _context.stop(); 71 | } 72 | }, _callee, this); 73 | })); 74 | function send(_x) { 75 | return _send.apply(this, arguments); 76 | } 77 | return send; 78 | }() 79 | }]); 80 | }(); -------------------------------------------------------------------------------- /lib/providers/sms/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 10 | var _logger = _interopRequireDefault(require("../logger")); 11 | var _elks = _interopRequireDefault(require("./46elks")); 12 | var _callr = _interopRequireDefault(require("./callr")); 13 | var _clickatell = _interopRequireDefault(require("./clickatell")); 14 | var _infobip = _interopRequireDefault(require("./infobip")); 15 | var _nexmo = _interopRequireDefault(require("./nexmo")); 16 | var _notificationCatcher = _interopRequireDefault(require("./notificationCatcher")); 17 | var _ovh = _interopRequireDefault(require("./ovh")); 18 | var _plivo = _interopRequireDefault(require("./plivo")); 19 | var _twilio = _interopRequireDefault(require("./twilio")); 20 | var _seven = _interopRequireDefault(require("./seven")); 21 | var _excluded = ["type"]; // Types 22 | function factory(_ref) { 23 | var type = _ref.type, 24 | config = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 25 | switch (type) { 26 | // Development 27 | case 'logger': 28 | return new _logger["default"](config, 'sms'); 29 | case 'notificationcatcher': 30 | return new _notificationCatcher["default"]('sms'); 31 | 32 | // Custom 33 | case 'custom': 34 | return config; 35 | 36 | // Providers 37 | case '46elks': 38 | return new _elks["default"](config); 39 | case 'callr': 40 | return new _callr["default"](config); 41 | case 'clickatell': 42 | return new _clickatell["default"](config); 43 | case 'infobip': 44 | return new _infobip["default"](config); 45 | case 'nexmo': 46 | return new _nexmo["default"](config); 47 | case 'ovh': 48 | return new _ovh["default"](config); 49 | case 'plivo': 50 | return new _plivo["default"](config); 51 | case 'twilio': 52 | return new _twilio["default"](config); 53 | case 'seven': 54 | return new _seven["default"](config); 55 | default: 56 | throw new Error("Unknown sms provider \"".concat(type, "\".")); 57 | } 58 | } -------------------------------------------------------------------------------- /lib/providers/voice/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 10 | var _logger = _interopRequireDefault(require("../logger")); 11 | var _notificationCatcher = _interopRequireDefault(require("./notificationCatcher")); 12 | var _twilio = _interopRequireDefault(require("./twilio")); 13 | var _excluded = ["type"]; 14 | // Types 15 | 16 | function factory(_ref) { 17 | var type = _ref.type, 18 | config = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 19 | switch (type) { 20 | // Development 21 | case 'logger': 22 | return new _logger["default"](config, 'voice'); 23 | case 'notificationcatcher': 24 | return new _notificationCatcher["default"]('voice'); 25 | 26 | // Custom 27 | case 'custom': 28 | return config; 29 | 30 | // Providers 31 | case 'twilio': 32 | return new _twilio["default"](config); 33 | default: 34 | throw new Error("Unknown voice provider \"".concat(type, "\".")); 35 | } 36 | } -------------------------------------------------------------------------------- /lib/providers/webpush/gcm.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify")); 11 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 12 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 13 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 14 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 15 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); 16 | var _webPush = _interopRequireDefault(require("web-push")); 17 | var _excluded = ["subscription"]; 18 | // Types 19 | var WebpushGcmProvider = exports["default"] = /*#__PURE__*/function () { 20 | function WebpushGcmProvider(_ref) { 21 | var gcmAPIKey = _ref.gcmAPIKey, 22 | vapidDetails = _ref.vapidDetails, 23 | ttl = _ref.ttl, 24 | headers = _ref.headers; 25 | (0, _classCallCheck2["default"])(this, WebpushGcmProvider); 26 | (0, _defineProperty2["default"])(this, "id", 'webpush-gcm-provider'); 27 | this.options = { 28 | TTL: ttl, 29 | headers: headers 30 | }; 31 | if (gcmAPIKey) { 32 | _webPush["default"].setGCMAPIKey(gcmAPIKey); 33 | } 34 | if (vapidDetails) { 35 | var subject = vapidDetails.subject, 36 | publicKey = vapidDetails.publicKey, 37 | privateKey = vapidDetails.privateKey; 38 | _webPush["default"].setVapidDetails(subject, publicKey, privateKey); 39 | } 40 | } 41 | return (0, _createClass2["default"])(WebpushGcmProvider, [{ 42 | key: "send", 43 | value: function () { 44 | var _send = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 45 | var _ref2, subscription, rest, result; 46 | return _regenerator["default"].wrap(function _callee$(_context) { 47 | while (1) switch (_context.prev = _context.next) { 48 | case 0: 49 | if (!request.customize) { 50 | _context.next = 6; 51 | break; 52 | } 53 | _context.next = 3; 54 | return request.customize(this.id, request); 55 | case 3: 56 | _context.t0 = _context.sent; 57 | _context.next = 7; 58 | break; 59 | case 6: 60 | _context.t0 = request; 61 | case 7: 62 | _ref2 = _context.t0; 63 | subscription = _ref2.subscription; 64 | rest = (0, _objectWithoutProperties2["default"])(_ref2, _excluded); 65 | _context.next = 12; 66 | return _webPush["default"].sendNotification(subscription, (0, _stringify["default"])(rest), this.options); 67 | case 12: 68 | result = _context.sent; 69 | return _context.abrupt("return", result.headers.location); 70 | case 14: 71 | case "end": 72 | return _context.stop(); 73 | } 74 | }, _callee, this); 75 | })); 76 | function send(_x) { 77 | return _send.apply(this, arguments); 78 | } 79 | return send; 80 | }() 81 | }]); 82 | }(); -------------------------------------------------------------------------------- /lib/providers/webpush/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 10 | var _gcm = _interopRequireDefault(require("./gcm")); 11 | var _logger = _interopRequireDefault(require("../logger")); 12 | var _notificationCatcher = _interopRequireDefault(require("./notificationCatcher")); 13 | var _excluded = ["type"]; 14 | // Types 15 | 16 | function factory(_ref) { 17 | var type = _ref.type, 18 | config = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 19 | switch (type) { 20 | // Development 21 | case 'logger': 22 | return new _logger["default"](config, 'webpush'); 23 | case 'notificationcatcher': 24 | return new _notificationCatcher["default"]('webpush'); 25 | 26 | // Custom 27 | case 'custom': 28 | return config; 29 | 30 | // Providers 31 | case 'gcm': 32 | return new _gcm["default"](config); 33 | default: 34 | throw new Error("Unknown webpush provider \"".concat(type, "\".")); 35 | } 36 | } -------------------------------------------------------------------------------- /lib/providers/whatsapp/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectWithoutProperties")); 10 | var _infobip = _interopRequireDefault(require("./infobip")); 11 | var _logger = _interopRequireDefault(require("../logger")); 12 | var _notificationCatcher = _interopRequireDefault(require("./notificationCatcher")); 13 | var _excluded = ["type"]; 14 | // Types 15 | 16 | function factory(_ref) { 17 | var type = _ref.type, 18 | config = (0, _objectWithoutProperties2["default"])(_ref, _excluded); 19 | switch (type) { 20 | // Development 21 | case 'logger': 22 | return new _logger["default"](config, 'whatsapp'); 23 | case 'notificationcatcher': 24 | return new _notificationCatcher["default"]('whatsapp'); 25 | 26 | // Custom 27 | case 'custom': 28 | return config; 29 | 30 | // Providers 31 | case 'infobip': 32 | return new _infobip["default"](config); 33 | default: 34 | throw new Error("Unknown whatsapp provider \"".concat(type, "\".")); 35 | } 36 | } -------------------------------------------------------------------------------- /lib/strategies/providers/fallback.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _toArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/toArray")); 11 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 12 | var _logger = _interopRequireDefault(require("../../util/logger")); 13 | // Types 14 | function recursiveTry(_x, _x2) { 15 | return _recursiveTry.apply(this, arguments); 16 | } 17 | function _recursiveTry() { 18 | _recursiveTry = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(providers, request) { 19 | var _providers, current, others, id; 20 | return _regenerator["default"].wrap(function _callee$(_context) { 21 | while (1) switch (_context.prev = _context.next) { 22 | case 0: 23 | _providers = (0, _toArray2["default"])(providers), current = _providers[0], others = _providers.slice(1); 24 | _context.prev = 1; 25 | _context.next = 4; 26 | return current.send(request); 27 | case 4: 28 | id = _context.sent; 29 | return _context.abrupt("return", { 30 | providerId: current.id, 31 | id: id 32 | }); 33 | case 8: 34 | _context.prev = 8; 35 | _context.t0 = _context["catch"](1); 36 | _logger["default"].warn(current.id, _context.t0); 37 | if (!(others.length === 0)) { 38 | _context.next = 14; 39 | break; 40 | } 41 | // no more provider to try 42 | _context.t0.providerId = current.id; 43 | throw _context.t0; 44 | case 14: 45 | return _context.abrupt("return", recursiveTry(others, request)); 46 | case 15: 47 | case "end": 48 | return _context.stop(); 49 | } 50 | }, _callee, null, [[1, 8]]); 51 | })); 52 | return _recursiveTry.apply(this, arguments); 53 | } 54 | var strategyProvidersFallback = function strategyProvidersFallback(providers) { 55 | return function (request) { 56 | return recursiveTry(providers, request); 57 | }; 58 | }; 59 | var _default = exports["default"] = strategyProvidersFallback; -------------------------------------------------------------------------------- /lib/strategies/providers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = factory; 9 | var _keys = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/keys")); 10 | var _fallback = _interopRequireDefault(require("./fallback")); 11 | var _noFallback = _interopRequireDefault(require("./no-fallback")); 12 | var _roundrobin = _interopRequireDefault(require("./roundrobin")); 13 | // Types 14 | var providerStrategies = { 15 | fallback: _fallback["default"], 16 | 'no-fallback': _noFallback["default"], 17 | roundrobin: _roundrobin["default"] 18 | }; 19 | var strategies = (0, _keys["default"])(providerStrategies); 20 | function factory(channels) { 21 | return (0, _keys["default"])(channels).reduce(function (acc, key) { 22 | var optionStrategy = channels[key].multiProviderStrategy; 23 | if (typeof optionStrategy === 'function') { 24 | acc[key] = optionStrategy; 25 | } else if (strategies.includes(optionStrategy)) { 26 | acc[key] = providerStrategies[optionStrategy]; 27 | } else { 28 | throw new Error("\"".concat(optionStrategy, "\" is not a valid strategy. Strategy must be a function or ").concat(strategies.join('|'), ".")); 29 | } 30 | return acc; 31 | }, {}); 32 | } -------------------------------------------------------------------------------- /lib/strategies/providers/no-fallback.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); 10 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); 11 | var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray")); 12 | var _logger = _interopRequireDefault(require("../../util/logger")); 13 | // Types 14 | var strategyProvidersNoFallback = function strategyProvidersNoFallback(_ref) { 15 | var _ref2 = (0, _slicedToArray2["default"])(_ref, 1), 16 | provider = _ref2[0]; 17 | return /*#__PURE__*/function () { 18 | var _ref3 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator["default"].mark(function _callee(request) { 19 | var id; 20 | return _regenerator["default"].wrap(function _callee$(_context) { 21 | while (1) switch (_context.prev = _context.next) { 22 | case 0: 23 | _context.prev = 0; 24 | _context.next = 3; 25 | return provider.send(request); 26 | case 3: 27 | id = _context.sent; 28 | return _context.abrupt("return", { 29 | providerId: provider.id, 30 | id: id 31 | }); 32 | case 7: 33 | _context.prev = 7; 34 | _context.t0 = _context["catch"](0); 35 | _logger["default"].warn(provider.id, _context.t0); 36 | _context.t0.providerId = provider.id; 37 | throw _context.t0; 38 | case 12: 39 | case "end": 40 | return _context.stop(); 41 | } 42 | }, _callee, null, [[0, 7]]); 43 | })); 44 | return function (_x) { 45 | return _ref3.apply(this, arguments); 46 | }; 47 | }(); 48 | }; 49 | var _default = exports["default"] = strategyProvidersNoFallback; -------------------------------------------------------------------------------- /lib/strategies/providers/roundrobin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _fallback = _interopRequireDefault(require("./fallback")); 10 | // "statefull" strategy 11 | // Types 12 | function rotate(arr, forward) { 13 | // /!\ mute array, the mutation is "the state" 14 | if (forward) { 15 | arr.push(arr.shift()); 16 | } else { 17 | arr.unshift(arr.pop()); 18 | } 19 | return arr; 20 | } 21 | var strategyProvidersFallback = function strategyProvidersFallback(providers) { 22 | var rotatedProviders = rotate(providers, false); 23 | return function (request) { 24 | return (0, _fallback["default"])(rotate(rotatedProviders, true))(request); 25 | }; 26 | }; 27 | 28 | // /!\ not equivalent to (providers) => strategyFallback(rotate(providers)) because of memoization 29 | var _default = exports["default"] = strategyProvidersFallback; -------------------------------------------------------------------------------- /lib/util/aws/v4_credentials.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | _Object$defineProperty(exports, "__esModule", { 5 | value: true 6 | }); 7 | exports["default"] = void 0; 8 | var _crypto = require("../crypto"); 9 | /* https://github.com/aws/aws-sdk-js/blob/master/lib/signers/v4_credentials.js */ 10 | 11 | /** 12 | * @api private 13 | */ 14 | var cachedSecret = {}; 15 | 16 | /** 17 | * @api private 18 | */ 19 | var cacheQueue = []; 20 | 21 | /** 22 | * @api private 23 | */ 24 | var maxCacheEntries = 50; 25 | 26 | /** 27 | * @api private 28 | */ 29 | var v4Identifier = 'aws4_request'; 30 | var _default = exports["default"] = { 31 | /** 32 | * @api private 33 | * 34 | * @param date [String] 35 | * @param region [String] 36 | * @param serviceName [String] 37 | * @return [String] 38 | */ 39 | createScope: function createScope(date, region, serviceName) { 40 | return [date.substr(0, 8), region, serviceName, v4Identifier].join('/'); 41 | }, 42 | /** 43 | * @api private 44 | * 45 | * @param credentials [Credentials] 46 | * @param date [String] 47 | * @param region [String] 48 | * @param service [String] 49 | * @param shouldCache [Boolean] 50 | * @return [String] 51 | */ 52 | getSigningKey: function getSigningKey(credentials, date, region, service, shouldCache) { 53 | var credsIdentifier = (0, _crypto.hmac)(credentials.secretAccessKey, credentials.accessKeyId, 'base64'); 54 | var cacheKey = [credsIdentifier, date, region, service].join('_'); 55 | shouldCache = shouldCache !== false; 56 | if (shouldCache && cacheKey in cachedSecret) { 57 | return cachedSecret[cacheKey]; 58 | } 59 | var kDate = (0, _crypto.hmac)('AWS4' + credentials.secretAccessKey, date, 'buffer'); 60 | var kRegion = (0, _crypto.hmac)(kDate, region, 'buffer'); 61 | var kService = (0, _crypto.hmac)(kRegion, service, 'buffer'); 62 | var signingKey = (0, _crypto.hmac)(kService, v4Identifier, 'buffer'); 63 | if (shouldCache) { 64 | cachedSecret[cacheKey] = signingKey; 65 | cacheQueue.push(cacheKey); 66 | if (cacheQueue.length > maxCacheEntries) { 67 | // remove the oldest entry (not the least recently used) 68 | delete cachedSecret[cacheQueue.shift()]; 69 | } 70 | } 71 | return signingKey; 72 | }, 73 | /** 74 | * @api private 75 | * 76 | * Empties the derived signing key cache. Made available for testing purposes 77 | * only. 78 | */ 79 | emptyCache: function emptyCache() { 80 | cachedSecret = {}; 81 | cacheQueue = []; 82 | } 83 | }; -------------------------------------------------------------------------------- /lib/util/crypto.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.hmac = hmac; 9 | exports.sha256 = sha256; 10 | var _crypto = _interopRequireDefault(require("crypto")); 11 | function hmac(key, data, encoding) { 12 | return _crypto["default"].createHmac('sha256', key).update(typeof data === 'string' ? Buffer.from(data) : data).digest(encoding === 'buffer' ? undefined : encoding); 13 | } 14 | function sha256(data, encoding) { 15 | return _crypto["default"].createHash('sha256').update(typeof data === 'string' ? Buffer.from(data) : data).digest(encoding === 'buffer' ? undefined : encoding); 16 | } -------------------------------------------------------------------------------- /lib/util/dedupe.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | _Object$defineProperty(exports, "__esModule", { 5 | value: true 6 | }); 7 | exports["default"] = dedupe; 8 | function dedupe(array) { 9 | return array.filter(function (element, position) { 10 | return array.indexOf(element) === position; 11 | }); 12 | } -------------------------------------------------------------------------------- /lib/util/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 10 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 11 | var _winston = _interopRequireDefault(require("winston")); 12 | var Logger = /*#__PURE__*/function () { 13 | function Logger() { 14 | (0, _classCallCheck2["default"])(this, Logger); 15 | this.innerLogger = _winston["default"].createLogger(); 16 | this.configure({ 17 | transports: [new _winston["default"].transports.Console({ 18 | colorize: true 19 | })] 20 | }); 21 | } 22 | return (0, _createClass2["default"])(Logger, [{ 23 | key: "configure", 24 | value: function configure(options) { 25 | this.innerLogger.configure(options); 26 | } 27 | }, { 28 | key: "mute", 29 | value: function mute() { 30 | this.configure({ 31 | transports: [] 32 | }); 33 | } 34 | }, { 35 | key: "log", 36 | value: function log(level, info, extra) { 37 | this.innerLogger.log(level, info, extra); 38 | } 39 | }, { 40 | key: "error", 41 | value: function error(info, extra) { 42 | this.log('error', info, extra); 43 | } 44 | }, { 45 | key: "warn", 46 | value: function warn(info, extra) { 47 | this.log('warn', info, extra); 48 | } 49 | }, { 50 | key: "info", 51 | value: function info(_info, extra) { 52 | this.log('info', _info, extra); 53 | } 54 | }]); 55 | }(); 56 | var _default = exports["default"] = new Logger(); -------------------------------------------------------------------------------- /lib/util/registry.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); 10 | var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); 11 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty")); 12 | var Registry = /*#__PURE__*/function () { 13 | function Registry() { 14 | (0, _classCallCheck2["default"])(this, Registry); 15 | (0, _defineProperty2["default"])(this, "map", {}); 16 | } 17 | return (0, _createClass2["default"])(Registry, [{ 18 | key: "getInstance", 19 | value: function getInstance(key, getValueIfUndefined) { 20 | if (!this.map[key]) { 21 | this.map[key] = getValueIfUndefined(); 22 | } 23 | return this.map[key]; 24 | } 25 | }]); 26 | }(); 27 | var _default = exports["default"] = new Registry(); -------------------------------------------------------------------------------- /lib/util/request.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs2/core-js/object/define-property"); 4 | var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | var _objectDestructuringEmpty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectDestructuringEmpty")); 10 | var _extends2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/extends")); 11 | var _nodeFetch = _interopRequireDefault(require("node-fetch")); 12 | var _httpsProxyAgent = _interopRequireDefault(require("https-proxy-agent")); 13 | var _default = exports["default"] = function _default(url) { 14 | var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 15 | options = (0, _extends2["default"])({}, ((0, _objectDestructuringEmpty2["default"])(_ref), _ref)); 16 | if (!options.agent && process.env.NOTIFME_HTTP_PROXY) { 17 | options.agent = new _httpsProxyAgent["default"](process.env.NOTIFME_HTTP_PROXY); 18 | } 19 | return (0, _nodeFetch["default"])(url, options); 20 | }; -------------------------------------------------------------------------------- /src/models/provider-email.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { EmailRequestType } from './notification-request' 3 | 4 | // TODO?: provider APIs (SES, sendinblue, mailjet, mandrill, elasticemail...) 5 | export type EmailProviderType = { 6 | type: 'logger' 7 | } | { 8 | type: 'custom', 9 | id: string, 10 | send: (EmailRequestType) => Promise 11 | } | { 12 | // Doc: https://nodemailer.com/transports/sendmail/ 13 | type: 'sendmail', 14 | sendmail: true, 15 | path: string, // Defaults to 'sendmail' 16 | newline: 'windows' | 'unix', // Defaults to 'unix' 17 | args?: string[], 18 | attachDataUrls?: boolean, 19 | disableFileAccess?: boolean, 20 | disableUrlAccess?: boolean 21 | } | { 22 | // General options (Doc: https://nodemailer.com/smtp/) 23 | type: 'smtp', 24 | port?: 25 | 465 | 587, // Defaults to 587 25 | host?: string, // Defaults to 'localhost' 26 | auth: { 27 | type?: 'login', 28 | user: string, 29 | pass: string 30 | } | { 31 | type: 'oauth2', // Doc: https://nodemailer.com/smtp/oauth2/#oauth-3lo 32 | user: string, 33 | clientId?: string, 34 | clientSecret?: string, 35 | refreshToken?: string, 36 | accessToken?: string, 37 | expires?: string, 38 | accessUrl?: string 39 | } | { 40 | type: 'oauth2', // Doc: https://nodemailer.com/smtp/oauth2/#oauth-2lo 41 | user: string, 42 | serviceClient: string, 43 | privateKey?: string 44 | }, 45 | authMethod?: string, // Defaults to 'PLAIN' 46 | // TLS options (Doc: https://nodemailer.com/smtp/#tls-options) 47 | secure?: boolean, 48 | tls?: Object, // Doc: https://nodejs.org/api/tls.html#tls_class_tls_tlssocket 49 | ignoreTLS?: boolean, 50 | requireTLS?: boolean, 51 | // Connection options (Doc: https://nodemailer.com/smtp/#connection-options) 52 | name?: string, 53 | localAddress?: string, 54 | connectionTimeout?: number, 55 | greetingTimeout?: number, 56 | socketTimeout?: number, 57 | // Debug options (Doc: https://nodemailer.com/smtp/#debug-options) 58 | logger?: boolean, 59 | debug?: boolean, 60 | // Security options (Doc: https://nodemailer.com/smtp/#security-options) 61 | disableFileAccess?: boolean, 62 | disableUrlAccess?: boolean, 63 | // Pooling options (Doc: https://nodemailer.com/smtp/pooled/) 64 | pool?: boolean, 65 | maxConnections?: number, 66 | maxMessages?: number, 67 | rateDelta?: number, 68 | rateLimit?: number, 69 | // Proxy options (Doc: https://nodemailer.com/smtp/proxies/) 70 | proxy?: string 71 | } | { 72 | type: 'mailgun', 73 | apiKey: string, 74 | domainName: string 75 | } | { 76 | type: 'mandrill', 77 | apiKey: string 78 | } | { 79 | type: 'sendgrid', 80 | apiKey: string 81 | } | { 82 | type: 'ses', 83 | region: string, 84 | accessKeyId: string, 85 | secretAccessKey: string, 86 | sessionToken?: ?string 87 | } | { 88 | type: 'sparkpost', 89 | apiKey: string 90 | } 91 | -------------------------------------------------------------------------------- /src/models/provider-push.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { PushRequestType } from './notification-request' 3 | 4 | export type PushProviderType = { 5 | type: 'logger' 6 | } | { 7 | type: 'custom', 8 | id: string, 9 | send: (PushRequestType) => Promise 10 | } | { 11 | // Doc: https://github.com/node-apn/node-apn/blob/master/doc/provider.markdown 12 | type: 'apn', // Apple Push Notification 13 | token?: { 14 | key: string, 15 | keyId: string, 16 | teamId: string 17 | }, 18 | cert?: string, 19 | key?: string, 20 | ca?: {filename: string}[], 21 | pfx?: string, 22 | passphrase?: string, 23 | production?: boolean, 24 | rejectUnauthorized?: boolean, 25 | connectionRetryLimit?: number 26 | } | { 27 | // Doc: https://github.com/ToothlessGear/node-gcm 28 | type: 'fcm', // Firebase Cloud Messaging (previously called GCM, Google Cloud Messaging) 29 | id: string, 30 | phonegap?: boolean 31 | } | { 32 | // Doc: https://github.com/tjanczuk/wns 33 | type: 'wns', // Windows Push Notification 34 | clientId: string, 35 | clientSecret: string, 36 | notificationMethod: string // sendTileSquareBlock, sendTileSquareImage... 37 | } | { 38 | // Doc: https://github.com/umano/node-adm 39 | type: 'adm', // Amazon Device Messaging 40 | clientId: string, 41 | clientSecret: string 42 | } 43 | -------------------------------------------------------------------------------- /src/models/provider-slack.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // Types 3 | import type { SlackRequestType } from './notification-request' 4 | 5 | export type SlackProviderType = { 6 | type: 'logger' 7 | } | { 8 | type: 'custom', 9 | id: string, 10 | send: (SlackRequestType) => Promise 11 | } | { 12 | type: 'webhook', 13 | webhookUrl?: string // Can be overriden in request 14 | } 15 | -------------------------------------------------------------------------------- /src/models/provider-sms.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { SmsRequestType } from './notification-request' 3 | 4 | // TODO?: other SMS providers 5 | export type SmsProviderType = { 6 | type: 'logger' 7 | } | { 8 | type: 'custom', 9 | id: string, 10 | send: (SmsRequestType) => Promise 11 | } | { 12 | type: '46elks', 13 | apiUsername: string, 14 | apiPassword: string 15 | } | { 16 | type: 'callr', 17 | login: string, 18 | password: string 19 | } | { 20 | type: 'clickatell', 21 | apiKey: string // One-way integration API key 22 | } | { 23 | type: 'infobip', 24 | username: string, 25 | password: string 26 | } | { 27 | type: 'nexmo', 28 | apiKey: string, 29 | apiSecret: string 30 | } | { 31 | type: 'ovh', 32 | appKey: string, 33 | appSecret: string, 34 | consumerKey: string, 35 | account: string, 36 | host: string // https://github.com/ovh/node-ovh/blob/master/lib/endpoints.js 37 | } | { 38 | type: 'plivo', 39 | authId: string, 40 | authToken: string 41 | } | { 42 | type: 'twilio', 43 | accountSid: string, 44 | authToken: string 45 | } | { 46 | type: 'seven', 47 | apiKey: string, 48 | } 49 | -------------------------------------------------------------------------------- /src/models/provider-voice.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { VoiceRequestType } from './notification-request' 3 | 4 | // TODO?: other Voice providers 5 | export type VoiceProviderType = { 6 | type: 'logger' 7 | } | { 8 | type: 'custom', 9 | id: string, 10 | send: (VoiceRequestType) => Promise 11 | } | { 12 | type: 'twilio', 13 | accountSid: string, 14 | authToken: string 15 | } 16 | -------------------------------------------------------------------------------- /src/models/provider-webpush.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { WebpushRequestType } from './notification-request' 3 | 4 | // TODO?: onesignal, urbanairship, goroost, sendpulse, wonderpush, appboy... 5 | export type WebpushProviderType = { 6 | type: 'logger' 7 | } | { 8 | type: 'custom', 9 | id: string, 10 | send: (WebpushRequestType) => Promise 11 | } | { 12 | type: 'gcm', 13 | gcmAPIKey?: string, 14 | vapidDetails?: { 15 | subject: string, 16 | publicKey: string, 17 | privateKey: string 18 | }, 19 | ttl?: number, 20 | headers?: {[string]: string | number | boolean} 21 | } 22 | -------------------------------------------------------------------------------- /src/models/provider-whatsapp.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // Types 3 | import type { WhatsappRequestType } from './notification-request' 4 | 5 | export type WhatsappProviderType = { 6 | type: 'logger' 7 | } | { 8 | type: 'custom', 9 | id: string, 10 | send: (WhatsappRequestType) => Promise 11 | } | { 12 | type: 'infobip', 13 | baseUrl: string, 14 | apiKey: string, 15 | } 16 | -------------------------------------------------------------------------------- /src/providers/email/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import EmailLoggerProvider from '../logger' 3 | import EmailMailgunProvider from './mailgun' 4 | import EmailMandrillProvider from './mandrill' 5 | import EmailNotificationCatcherProvider from './notificationCatcher' 6 | import EmailSendGridProvider from './sendgrid' 7 | import EmailSesProvider from './ses' 8 | import EmailSendmailProvider from './sendmail' 9 | import EmailSmtpProvider from './smtp' 10 | import EmailSparkPostProvider from './sparkpost' 11 | // Types 12 | import type { EmailRequestType } from '../../models/notification-request' 13 | 14 | export interface EmailProviderType { 15 | id: string, 16 | send(request: EmailRequestType): Promise 17 | } 18 | 19 | export default function factory ({ type, ...config }: Object): EmailProviderType { 20 | switch (type) { 21 | // Development 22 | case 'logger': 23 | return new EmailLoggerProvider(config, 'email') 24 | 25 | case 'notificationcatcher': 26 | return new EmailNotificationCatcherProvider('email') 27 | 28 | // Custom 29 | case 'custom': 30 | return config 31 | 32 | // Protocols 33 | case 'sendmail': 34 | return new EmailSendmailProvider(config) 35 | 36 | case 'smtp': 37 | return new EmailSmtpProvider(config) 38 | 39 | // Providers 40 | case 'mailgun': 41 | return new EmailMailgunProvider(config) 42 | 43 | case 'mandrill': 44 | return new EmailMandrillProvider(config) 45 | 46 | case 'sendgrid': 47 | return new EmailSendGridProvider(config) 48 | 49 | case 'ses': 50 | return new EmailSesProvider(config) 51 | 52 | case 'sparkpost': 53 | return new EmailSparkPostProvider(config) 54 | 55 | default: 56 | throw new Error(`Unknown email provider "${type}".`) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/providers/email/mailgun.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | import FormData from 'form-data' 4 | // types 5 | import type { EmailRequestType } from '../../models/notification-request' 6 | 7 | export default class EmailMailgunProvider { 8 | id: string = 'email-mailgun-provider' 9 | apiKeyBase64: string 10 | domainName: string 11 | host: string 12 | version: string 13 | 14 | constructor (config: Object) { 15 | this.apiKeyBase64 = Buffer.from(`api:${config.apiKey}`).toString('base64') 16 | this.domainName = config.domainName 17 | this.host = config.host || 'api.mailgun.net' 18 | this.version = config.version || 'v3' 19 | } 20 | 21 | async send (request: EmailRequestType): Promise { 22 | const { id, userId, from, replyTo, subject, html, text, headers, to, cc, bcc, attachments } = 23 | request.customize ? (await request.customize(this.id, request)) : request 24 | const form = new FormData() 25 | form.append('from', from) 26 | form.append('to', to) 27 | form.append('subject', subject) 28 | if (text) form.append('text', text) 29 | if (html) form.append('html', html) 30 | if (replyTo) form.append('h:Reply-To', replyTo) 31 | if (cc && cc.length > 0) cc.forEach((email) => form.append('cc', email)) 32 | if (bcc && bcc.length > 0) bcc.forEach((email) => form.append('bcc', email)) 33 | if (attachments && attachments.length > 0) { 34 | attachments.forEach(({ contentType, filename, content }) => { 35 | form.append('attachment', content, { filename, contentType }) 36 | }) 37 | } 38 | if (headers) Object.keys(headers).forEach((header) => form.append(`h:${header}`, headers[header])) 39 | if (id) form.append('v:Notification-Id', id) 40 | if (userId) form.append('v:User-Id', userId) 41 | 42 | const response = await fetch(`https://${this.host}/${this.version}/${this.domainName}/messages`, { 43 | method: 'POST', 44 | headers: { 45 | Authorization: `Basic ${this.apiKeyBase64}`, 46 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 47 | }, 48 | body: form 49 | }) 50 | 51 | const responseBody = await response.json() 52 | if (response.ok) { 53 | return responseBody.id 54 | } else { 55 | throw new Error(`${response.status} - ${responseBody.message}`) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/providers/email/mandrill.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // types 4 | import type { EmailRequestType } from '../../models/notification-request' 5 | 6 | export default class EmailMandrillProvider { 7 | id: string = 'email-mandrill-provider' 8 | apiKey: string 9 | 10 | constructor (config: Object) { 11 | this.apiKey = config.apiKey 12 | } 13 | 14 | async send (request: EmailRequestType): Promise { 15 | const { id, userId, from, replyTo, subject, html, text, headers, to, cc, bcc, attachments } = 16 | request.customize ? (await request.customize(this.id, request)) : request 17 | const response = await fetch('https://mandrillapp.com/api/1.0/messages/send.json', { 18 | method: 'POST', 19 | headers: { 20 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)', 21 | 'Content-Type': 'application/json' 22 | }, 23 | body: JSON.stringify({ 24 | key: this.apiKey, 25 | message: { 26 | from_email: from, 27 | to: [ 28 | { email: to, type: 'to' }, 29 | ...(cc && cc.length ? cc.map(email => ({ email, type: 'cc' })) : []), 30 | ...(bcc && bcc.length ? bcc.map(email => ({ email, type: 'bcc' })) : []) 31 | ], 32 | subject, 33 | text, 34 | html, 35 | headers: { 36 | ...(replyTo ? { 'Reply-To': replyTo } : null), 37 | ...headers 38 | }, 39 | ...(attachments && attachments.length ? { 40 | attachments: attachments.map(({ contentType, filename, content }) => { 41 | return { 42 | type: contentType, 43 | name: filename, 44 | content: (typeof content === 'string' ? Buffer.from(content) : content).toString('base64') 45 | } 46 | }) 47 | } : null), 48 | metadata: { 49 | id, 50 | userId 51 | } 52 | }, 53 | async: false 54 | }) 55 | }) 56 | 57 | const responseBody = await response.json() 58 | if (response.ok && responseBody.length > 0) { 59 | return responseBody[0]._id 60 | } else { 61 | const message = Object.keys(responseBody).map((key) => `${key}: ${responseBody[key]}`).join(', ') 62 | throw new Error(`${response.status} - ${message}`) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/providers/email/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotificationCatcherProvider from '../notificationCatcherProvider' 3 | // Types 4 | import type { EmailRequestType } from '../../models/notification-request' 5 | 6 | export default class EmailNotificationCatcherProvider extends NotificationCatcherProvider { 7 | async send (request: EmailRequestType): Promise { 8 | const { to, from, html, text, subject, replyTo, attachments } = 9 | request.customize ? (await request.customize(this.id, request)) : request 10 | return this.sendToCatcher({ 11 | to, 12 | from, 13 | html, 14 | text, 15 | subject, 16 | replyTo, 17 | attachments, 18 | headers: { 19 | 'X-to': `[email] ${to}` 20 | } 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/providers/email/sendgrid.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import crypto from 'crypto' 3 | import fetch from '../../util/request' 4 | // Types 5 | import type { EmailRequestType } from '../../models/notification-request' 6 | 7 | export default class EmailSendGridProvider { 8 | id: string = 'email-sendgrid-provider' 9 | apiKey: string 10 | 11 | constructor (config: Object) { 12 | this.apiKey = config.apiKey 13 | } 14 | 15 | async send (request: EmailRequestType): Promise { 16 | const { id, userId, from, replyTo, subject, html, text, headers, to, cc, bcc, attachments } = 17 | request.customize ? (await request.customize(this.id, request)) : request 18 | const generatedId = id || crypto.randomBytes(16).toString('hex') 19 | const response = await fetch('https://api.sendgrid.com/v3/mail/send', { 20 | method: 'POST', 21 | headers: { 22 | Authorization: `Bearer ${this.apiKey}`, 23 | 'Content-Type': 'application/json', 24 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 25 | }, 26 | body: JSON.stringify({ 27 | personalizations: [{ 28 | to: [{ email: to }], 29 | ...(cc && cc.length > 0 ? { cc: cc.map((email) => ({ email })) } : null), 30 | ...(bcc && bcc.length > 0 ? { bcc: bcc.map((email) => ({ email })) } : null) 31 | }], 32 | from: { email: from }, 33 | ...(replyTo ? { reply_to: { email: replyTo } } : null), 34 | subject, 35 | content: [ 36 | ...(text ? [{ type: 'text/plain', value: text }] : []), 37 | ...(html ? [{ type: 'text/html', value: html }] : []) 38 | ], 39 | headers, 40 | custom_args: { id: generatedId, userId }, 41 | ...(attachments && attachments.length > 0 ? { 42 | attachments: attachments.map(({ contentType, filename, content }) => 43 | ({ 44 | type: contentType, 45 | filename, 46 | content: (typeof content === 'string' ? Buffer.from(content) : content).toString('base64') 47 | })) 48 | } : null) 49 | }) 50 | }) 51 | 52 | if (response.ok) { 53 | return generatedId 54 | } else { 55 | const responseBody = await response.json() 56 | const [firstError] = responseBody.errors 57 | const message = Object.keys(firstError).map((key) => `${key}: ${firstError[key]}`).join(', ') 58 | throw new Error(`${response.status} - ${message}`) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/providers/email/sendmail.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import nodemailer from 'nodemailer' 3 | // Types 4 | import type { EmailRequestType } from '../../models/notification-request' 5 | 6 | export default class EmailSendmailProvider { 7 | id: string = 'email-sendmail-provider' 8 | transporter: Object 9 | 10 | constructor (config: Object) { 11 | this.transporter = nodemailer.createTransport(config) 12 | } 13 | 14 | async send (request: EmailRequestType): Promise { 15 | const { customize, ...rest } = request.customize ? (await request.customize(this.id, request)) : request 16 | const result = await this.transporter.sendMail(rest) 17 | return result.messageId 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/providers/email/ses.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import AWSSignersV4 from '../../util/aws/v4' 3 | import { sha256 } from '../../util/crypto' 4 | import fetch from '../../util/request' 5 | import MailComposer from 'nodemailer/lib/mail-composer' 6 | import qs from 'querystring' 7 | // types 8 | import type { EmailRequestType } from '../../models/notification-request' 9 | 10 | export default class EmailSesProvider { 11 | id: string = 'email-ses-provider' 12 | credentials: { 13 | region: string, 14 | accessKeyId: string, 15 | secretAccessKey: string, 16 | sessionToken: ?string 17 | } 18 | 19 | constructor ({ region, accessKeyId, secretAccessKey, sessionToken }: Object) { 20 | this.credentials = { region, accessKeyId, secretAccessKey, sessionToken } 21 | } 22 | 23 | async send (request: EmailRequestType): Promise { 24 | if ( 25 | request.text && 26 | typeof request.text !== 'string' && 27 | !(request.text instanceof Buffer) && 28 | !(request.text instanceof Uint8Array) 29 | ) { 30 | throw new Error( 31 | 'The "chunk" argument must be of type string or an instance of Buffer or Uint8Array.' 32 | ) 33 | } 34 | 35 | const { region } = this.credentials 36 | const host = `email.${region}.amazonaws.com` 37 | const raw = (await this.getRaw( 38 | request.customize ? (await request.customize(this.id, request)) : request) 39 | ).toString('base64') 40 | const body = qs.stringify({ 41 | Action: 'SendRawEmail', 42 | Version: '2010-12-01', 43 | 'RawMessage.Data': raw 44 | }) 45 | const apiRequest = { 46 | method: 'POST', 47 | path: '/', 48 | headers: { 49 | Host: host, 50 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 51 | 'X-Amz-Content-Sha256': sha256(body, 'hex'), 52 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 53 | }, 54 | body, 55 | region 56 | } 57 | const signer = new AWSSignersV4(apiRequest, 'ses') 58 | signer.addAuthorization(this.credentials, new Date()) 59 | 60 | const response = await fetch(`https://${host}${apiRequest.path}`, apiRequest) 61 | 62 | const responseText = await response.text() 63 | if (response.ok && responseText.includes('')) { 64 | return responseText.match(/(.*)<\/MessageId>/)[1] 65 | } else { 66 | throw new Error(`${response.status} - ${responseText}`) 67 | } 68 | } 69 | 70 | async getRaw ({ customize, ...request }: EmailRequestType): Promise { 71 | const email = new MailComposer(request).compile() 72 | email.keepBcc = true 73 | return email.build() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/providers/email/smtp.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import nodemailer from 'nodemailer' 3 | // Types 4 | import type { EmailRequestType } from '../../models/notification-request' 5 | 6 | export default class EmailSmtpProvider { 7 | id: string = 'email-smtp-provider' 8 | transporter: Object 9 | 10 | constructor (config: Object | string) { 11 | this.transporter = nodemailer.createTransport(config) 12 | } 13 | 14 | async send (request: EmailRequestType): Promise { 15 | const { customize, ...rest } = request.customize ? (await request.customize(this.id, request)) : request 16 | const result = await this.transporter.sendMail(rest) 17 | return result.messageId 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/providers/email/sparkpost.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // types 4 | import type { EmailRequestType } from '../../models/notification-request' 5 | 6 | export default class EmailSparkPostProvider { 7 | id: string = 'email-sparkpost-provider' 8 | apiKey: string 9 | 10 | constructor (config: Object) { 11 | this.apiKey = config.apiKey 12 | } 13 | 14 | async send (request: EmailRequestType): Promise { 15 | const { id, userId, from, replyTo, subject, html, text, headers, to, cc, bcc, attachments } = 16 | request.customize ? (await request.customize(this.id, request)) : request 17 | const response = await fetch('https://api.sparkpost.com/api/v1/transmissions', { 18 | method: 'POST', 19 | headers: { 20 | Authorization: this.apiKey, 21 | 'Content-Type': 'application/json', 22 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 23 | }, 24 | body: JSON.stringify({ 25 | options: { 26 | transactional: true 27 | }, 28 | content: { 29 | from, 30 | reply_to: replyTo, 31 | subject, 32 | html, 33 | text, 34 | headers: { 35 | ...headers, 36 | ...(cc && cc.length > 0 ? { CC: cc.join(',') } : null) 37 | }, 38 | attachments: (attachments || []).map(({ contentType, filename, content }) => 39 | ({ 40 | type: contentType, 41 | name: filename, 42 | data: (typeof content === 'string' ? Buffer.from(content) : content).toString('base64') 43 | })) 44 | }, 45 | recipients: [ 46 | { address: { email: to } }, 47 | ...(cc || []).map((email) => ({ address: { email, header_to: to } })), 48 | ...(bcc || []).map((email) => ({ address: { email, header_to: to } })) 49 | ], 50 | metadata: { id, userId } 51 | }) 52 | }) 53 | 54 | const responseBody = await response.json() 55 | if (response.ok) { 56 | return responseBody.results.id 57 | } else { 58 | const [firstError] = responseBody.errors 59 | const message = Object.keys(firstError).map((key) => `${key}: ${firstError[key]}`).join(', ') 60 | throw new Error(`${response.status} - ${message}`) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/providers/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import emailFactory from './email' 3 | import pushFactory from './push' 4 | import smsFactory from './sms' 5 | import voiceFactory from './voice' 6 | import webpushFactory from './webpush' 7 | import slackFactory from './slack' 8 | import whatsappFactory from './whatsapp' 9 | // Types 10 | import type { ChannelType } from '../index' 11 | import type { RequestType } from '../models/notification-request' 12 | 13 | export type ChannelOptionsType = {[ChannelType]: {providers: Object[]}} 14 | 15 | export interface ProviderType { 16 | id: string; 17 | send(RequestType): Promise; 18 | } 19 | 20 | export type ProvidersType = {[ChannelType]: ProviderType[]} 21 | 22 | export default function factory (channels: ChannelOptionsType): ProvidersType { 23 | return (Object.keys(channels): any).reduce((acc, key: ChannelType): ProvidersType => { 24 | acc[key] = channels[key].providers.map((config) => { 25 | switch (key) { 26 | case 'email': 27 | return emailFactory(config) 28 | 29 | case 'sms': 30 | return smsFactory(config) 31 | 32 | case 'voice': 33 | return voiceFactory(config) 34 | 35 | case 'push': 36 | return pushFactory(config) 37 | 38 | case 'webpush': 39 | return webpushFactory(config) 40 | 41 | case 'slack': 42 | return slackFactory(config) 43 | 44 | case 'whatsapp': 45 | return whatsappFactory(config) 46 | 47 | default: 48 | return config 49 | } 50 | }) 51 | 52 | return acc 53 | }, {}) 54 | } 55 | -------------------------------------------------------------------------------- /src/providers/logger.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import logger from '../util/logger' 3 | // Types 4 | import type { ChannelType } from '../index' 5 | import type { RequestType } from '../models/notification-request' 6 | 7 | export default class LoggerProvider { 8 | id: string 9 | channel: ChannelType 10 | 11 | constructor (config: Object, channel: ChannelType) { 12 | this.id = `${channel}-logger-provider` 13 | this.channel = channel 14 | } 15 | 16 | async send (request: RequestType): Promise { 17 | logger.info(`[${this.channel.toUpperCase()}] Sent by "${this.id}":`) 18 | logger.info(request) 19 | return `id-${Math.round(Math.random() * 1000000000)}` 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/providers/notificationCatcherProvider.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import EmailSmtpProvider from './email/smtp' 3 | // Types 4 | import type { ChannelType } from '../index' 5 | import type { EmailRequestType } from '../models/notification-request' 6 | 7 | export default class NotificationCatcherProvider { 8 | id: string 9 | provider: EmailSmtpProvider 10 | 11 | static getConfig (channels: ChannelType[]) { 12 | return channels.reduce((config, channel: any) => ({ 13 | ...config, 14 | [channel]: { 15 | providers: [{ type: 'notificationcatcher' }], 16 | multiProviderStrategy: 'no-fallback' 17 | } 18 | }), {}) 19 | } 20 | 21 | constructor (channel: ChannelType) { 22 | this.id = `${channel}-notificationcatcher-provider` 23 | 24 | const options = process.env.NOTIFME_CATCHER_OPTIONS || { 25 | port: 1025, 26 | ignoreTLS: true 27 | } 28 | 29 | this.provider = new EmailSmtpProvider(options) 30 | } 31 | 32 | async sendToCatcher (request: EmailRequestType): Promise { 33 | return this.provider.send(request) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/providers/push/adm.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import PushNotifications from 'node-pushnotifications' 3 | // Types 4 | import type { PushRequestType } from '../../models/notification-request' 5 | 6 | export default class PushAdmProvider { 7 | id: string = 'push-adm-provider' 8 | transporter: Object 9 | 10 | constructor (config: Object) { 11 | this.transporter = new PushNotifications({ 12 | adm: { 13 | ...config, 14 | client_id: config.clientId, 15 | client_secret: config.clientSecret 16 | } 17 | }) 18 | } 19 | 20 | async send (request: PushRequestType): Promise { 21 | const { registrationToken, ...rest } = 22 | request.customize ? (await request.customize(this.id, request)) : request 23 | const result = await this.transporter.send([registrationToken], rest) 24 | if (result[0].failure > 0) { 25 | throw new Error(result[0].message[0].error) 26 | } else { 27 | return result[0].message[0].messageId 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/providers/push/apn.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import PushNotifications from 'node-pushnotifications' 3 | // Types 4 | import type { PushRequestType } from '../../models/notification-request' 5 | 6 | export default class PushApnProvider { 7 | id: string = 'push-apn-provider' 8 | transporter: Object 9 | 10 | constructor (config: Object) { 11 | this.transporter = new PushNotifications({ apn: config }) 12 | } 13 | 14 | async send (request: PushRequestType): Promise { 15 | const { registrationToken, ...rest } = 16 | request.customize ? (await request.customize(this.id, request)) : request 17 | const result = await this.transporter.send([registrationToken], rest) 18 | if (result[0].failure > 0) { 19 | throw new Error(result[0].message[0].error) 20 | } else { 21 | return result[0].message[0].messageId 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/providers/push/fcm.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import PushNotifications from 'node-pushnotifications' 3 | // Types 4 | import type { PushRequestType } from '../../models/notification-request' 5 | 6 | export default class PushFcmProvider { 7 | id: string = 'push-fcm-provider' 8 | transporter: Object 9 | 10 | constructor (config: Object) { 11 | this.transporter = new PushNotifications({ gcm: config }) 12 | } 13 | 14 | async send (request: PushRequestType): Promise { 15 | const { registrationToken, ...rest } = 16 | request.customize ? (await request.customize(this.id, request)) : request 17 | const result = await this.transporter.send([registrationToken], rest) 18 | if (result[0].failure > 0) { 19 | throw new Error(result[0].message[0].error) 20 | } else { 21 | return result[0].message[0].messageId 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/providers/push/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import PushAdmProvider from './adm' 3 | import PushApnProvider from './apn' 4 | import PushFcmProvider from './fcm' 5 | import PushLoggerProvider from '../logger' 6 | import PushNotificationCatcherProvider from './notificationCatcher' 7 | import PushWnsProvider from './wns' 8 | // Types 9 | import type { PushRequestType } from '../../models/notification-request' 10 | 11 | export interface PushProviderType { 12 | id: string, 13 | send(request: PushRequestType): Promise 14 | } 15 | 16 | export default function factory ({ type, ...config }: Object): PushProviderType { 17 | switch (type) { 18 | // Development 19 | case 'logger': 20 | return new PushLoggerProvider(config, 'push') 21 | 22 | case 'notificationcatcher': 23 | return new PushNotificationCatcherProvider('push') 24 | 25 | // Custom 26 | case 'custom': 27 | return config 28 | 29 | // Providers 30 | case 'adm': 31 | return new PushAdmProvider(config) 32 | 33 | case 'apn': 34 | return new PushApnProvider(config) 35 | 36 | case 'fcm': 37 | return new PushFcmProvider(config) 38 | 39 | case 'wns': 40 | return new PushWnsProvider(config) 41 | 42 | default: 43 | throw new Error(`Unknown push provider "${type}".`) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/providers/push/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotificationCatcherProvider from '../notificationCatcherProvider' 3 | // Types 4 | import type { PushRequestType } from '../../models/notification-request' 5 | 6 | export default class PushNotificationCatcherProvider extends NotificationCatcherProvider { 7 | async send (request: PushRequestType): Promise { 8 | const { registrationToken, title, ...rest } = 9 | request.customize ? (await request.customize(this.id, request)) : request 10 | return this.sendToCatcher({ 11 | to: 'user@push.me', 12 | from: '-', 13 | subject: `${title.substring(0, 20)}${title.length > 20 ? '...' : ''}`, 14 | headers: { 15 | 'X-type': 'push', 16 | 'X-to': `[push] ${registrationToken.substring(0, 20)}...`, 17 | 'X-payload': JSON.stringify({ title, ...rest }) 18 | } 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/providers/push/wns.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import PushNotifications from 'node-pushnotifications' 3 | // Types 4 | import type { PushRequestType } from '../../models/notification-request' 5 | 6 | export default class PushWnsProvider { 7 | id: string = 'push-wns-provider' 8 | transporter: Object 9 | 10 | constructor (config: Object) { 11 | this.transporter = new PushNotifications({ 12 | wns: { 13 | ...config, 14 | client_id: config.clientId, 15 | client_secret: config.clientSecret 16 | } 17 | }) 18 | } 19 | 20 | async send (request: PushRequestType): Promise { 21 | const { registrationToken, ...rest } = 22 | request.customize ? (await request.customize(this.id, request)) : request 23 | const result = await this.transporter.send([registrationToken], rest) 24 | if (result[0].failure > 0) { 25 | throw new Error(result[0].message[0].error) 26 | } else { 27 | return result[0].message[0].messageId 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/providers/slack/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import SlackProvider from './slack' 3 | import SlackLoggingProvider from '../logger' 4 | import SlackNotificationCatcherProvider from './notificationCatcher' 5 | // Types 6 | import type { SlackRequestType } from '../../models/notification-request' 7 | 8 | export interface SlackProviderType { 9 | id: string, 10 | send(request: SlackRequestType): Promise 11 | } 12 | 13 | export default function factory ({ type, ...config }: Object): SlackProviderType { 14 | switch (type) { 15 | // Development 16 | case 'logger': 17 | return new SlackLoggingProvider(config, 'slack') 18 | 19 | case 'notificationcatcher': 20 | return new SlackNotificationCatcherProvider('slack') 21 | 22 | // Custom 23 | case 'custom': 24 | return config 25 | 26 | // Providers 27 | case 'webhook': 28 | return new SlackProvider(config) 29 | 30 | default: 31 | throw new Error(`Unknown slack provider "${type}".`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/slack/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotificationCatcherProvider from '../notificationCatcherProvider' 3 | // Types 4 | import type { SlackRequestType } from '../../models/notification-request' 5 | 6 | export default class SlackCatcherProvider extends NotificationCatcherProvider { 7 | async send (request: SlackRequestType): Promise { 8 | const { text } = request.customize ? (await request.customize(this.id, request)) : request 9 | this.sendToCatcher({ 10 | to: 'public.channel@slack', 11 | from: '-', 12 | subject: `${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`, 13 | text, 14 | headers: { 15 | 'X-type': 'slack', 16 | 'X-to': '[slack public channel]' 17 | } 18 | }) 19 | return '' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/providers/slack/slack.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { SlackRequestType } from '../../models/notification-request' 5 | 6 | export default class SlackProvider { 7 | id: string = 'slack-provider' 8 | webhookUrl: string 9 | 10 | constructor (config: Object) { 11 | this.webhookUrl = config.webhookUrl 12 | } 13 | 14 | async send (request: SlackRequestType): Promise { 15 | const { webhookUrl, ...rest } = request.customize ? (await request.customize(this.id, request)) : request 16 | const apiRequest = { 17 | method: 'POST', 18 | body: JSON.stringify(rest) 19 | } 20 | const response = await fetch(webhookUrl || this.webhookUrl, apiRequest) 21 | 22 | if (response.ok) { 23 | return '' // Slack API only returns 'ok' 24 | } else { 25 | const responseText = await response.text() 26 | throw new Error(`${response.status} - ${responseText}`) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/providers/sms/46elks.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | import qs from 'querystring' 4 | // Types 5 | import type { SmsRequestType } from '../../models/notification-request' 6 | 7 | export default class Sms46elksProvider { 8 | id: string = 'sms-46elks-provider' 9 | apiKey: string 10 | 11 | constructor ({ apiUsername, apiPassword }: Object) { 12 | this.apiKey = Buffer.from(`${apiUsername}:${apiPassword}`).toString('base64') 13 | } 14 | 15 | /* 16 | * Note: 'type', 'nature', 'ttl', 'messageClass' are not supported. 17 | */ 18 | async send (request: SmsRequestType): Promise { 19 | const { from, to, text } = request.customize ? (await request.customize(this.id, request)) : request 20 | const response = await fetch('https://api.46elks.com/a1/sms', { 21 | method: 'POST', 22 | headers: { 23 | Authorization: `Basic ${this.apiKey}`, 24 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 25 | }, 26 | body: qs.stringify({ 27 | from, 28 | to, 29 | message: text 30 | }) 31 | }) 32 | 33 | if (response.ok) { 34 | const responseBody = await response.json() 35 | return responseBody.id 36 | } else { 37 | throw new Error(await response.text()) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/providers/sms/callr.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { SmsRequestType } from '../../models/notification-request' 5 | 6 | export default class SmsCallrProvider { 7 | id: string = 'sms-callr-provider' 8 | apiKey: string 9 | 10 | constructor ({ login, password }: Object) { 11 | this.apiKey = Buffer.from(`${login}:${password}`).toString('base64') 12 | } 13 | 14 | /* 15 | * Note: 'from', 'messageClass', 'ttl' are not supported. 16 | */ 17 | async send (request: SmsRequestType): Promise { 18 | const { id, userId, from, to, text, type, nature } = 19 | request.customize ? (await request.customize(this.id, request)) : request 20 | const response = await fetch('https://api.callr.com/rest/v1.1/sms', { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | Authorization: `Basic ${this.apiKey}`, 25 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 26 | }, 27 | body: JSON.stringify({ 28 | from, 29 | to, 30 | body: text, 31 | options: { 32 | force_encoding: type === 'unicode' ? 'UNICODE' : 'GSM', 33 | nature: nature === 'marketing' ? 'MARKETING' : 'ALERTING', 34 | ...(userId || id ? { user_data: userId || id } : null) 35 | } 36 | }) 37 | }) 38 | 39 | const responseBody = await response.json() 40 | if (response.ok) { 41 | return responseBody.data 42 | } else { 43 | const error = responseBody.data 44 | throw new Error(Object.keys(error).map((key) => `${key}: ${error[key]}`).join(', ')) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/providers/sms/clickatell.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { SmsRequestType } from '../../models/notification-request' 5 | 6 | export default class SmsClickatellProvider { 7 | id: string = 'sms-clickatell-provider' 8 | apiKey: string 9 | 10 | constructor (config: Object) { 11 | // One-way integration API key 12 | this.apiKey = config.apiKey 13 | } 14 | 15 | /* 16 | * Note: 'from', 'nature', 'messageClass' are not supported. 17 | */ 18 | async send (request: SmsRequestType): Promise { 19 | const { id, to, text, type, ttl } = request.customize ? (await request.customize(this.id, request)) : request 20 | const response = await fetch('https://platform.clickatell.com/messages', { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | Authorization: this.apiKey, 25 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 26 | }, 27 | body: JSON.stringify({ 28 | // no `from` for one-way integrations 29 | to: [to], 30 | content: text, 31 | charset: type === 'unicode' ? 'UCS2-BE' : 'UTF-8', 32 | ...(ttl ? { validityPeriod: ttl } : null), 33 | ...(id ? { clientMessageId: id } : null) 34 | }) 35 | }) 36 | 37 | if (response.ok) { 38 | const responseBody = await response.json() 39 | if (responseBody.error) { 40 | throw new Error(responseBody.error) 41 | } else { 42 | return responseBody.messages[0].apiMessageId 43 | } 44 | } else { 45 | throw new Error(await response.text()) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/providers/sms/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import SmsLoggerProvider from '../logger' 3 | import Sms46elksProvider from './46elks' 4 | import SmsCallrProvider from './callr' 5 | import SmsClickatellProvider from './clickatell' 6 | import SmsInfobipProvider from './infobip' 7 | import SmsNexmoProvider from './nexmo' 8 | import SmsNotificationCatcherProvider from './notificationCatcher' 9 | import SmsOvhProvider from './ovh' 10 | import SmsPlivoProvider from './plivo' 11 | import SmsTwilioProvider from './twilio' 12 | // Types 13 | import type { SmsRequestType } from '../../models/notification-request' 14 | import SmsSevenProvider from './seven' 15 | 16 | export interface SmsProviderType { 17 | id: string, 18 | send(request: SmsRequestType): Promise 19 | } 20 | 21 | export default function factory ({ type, ...config }: Object): SmsProviderType { 22 | switch (type) { 23 | // Development 24 | case 'logger': 25 | return new SmsLoggerProvider(config, 'sms') 26 | 27 | case 'notificationcatcher': 28 | return new SmsNotificationCatcherProvider('sms') 29 | 30 | // Custom 31 | case 'custom': 32 | return config 33 | 34 | // Providers 35 | case '46elks': 36 | return new Sms46elksProvider(config) 37 | 38 | case 'callr': 39 | return new SmsCallrProvider(config) 40 | 41 | case 'clickatell': 42 | return new SmsClickatellProvider(config) 43 | 44 | case 'infobip': 45 | return new SmsInfobipProvider(config) 46 | 47 | case 'nexmo': 48 | return new SmsNexmoProvider(config) 49 | 50 | case 'ovh': 51 | return new SmsOvhProvider(config) 52 | 53 | case 'plivo': 54 | return new SmsPlivoProvider(config) 55 | 56 | case 'twilio': 57 | return new SmsTwilioProvider(config) 58 | 59 | case 'seven': 60 | return new SmsSevenProvider(config) 61 | 62 | default: 63 | throw new Error(`Unknown sms provider "${type}".`) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/providers/sms/infobip.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { SmsRequestType } from '../../models/notification-request' 5 | 6 | export default class SmsInfobipProvider { 7 | id: string = 'sms-infobip-provider' 8 | apiKey: string 9 | 10 | constructor ({ username, password }: Object) { 11 | this.apiKey = Buffer.from(`${username}:${password}`).toString('base64') 12 | } 13 | 14 | /* 15 | * Note: 'nature', 'messageClass', 'type', 'ttl' are not supported. 16 | */ 17 | async send (request: SmsRequestType): Promise { 18 | const { from, to, text } = request.customize ? (await request.customize(this.id, request)) : request 19 | const response = await fetch('https://api.infobip.com/sms/1/text/single', { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | Authorization: `Basic ${this.apiKey}`, 24 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 25 | }, 26 | body: JSON.stringify({ 27 | from, 28 | to, 29 | text 30 | }) 31 | }) 32 | 33 | const responseBody = await response.json() 34 | if (response.ok) { 35 | const message = responseBody.messages[0] 36 | if (message.status.groupId === 1) { 37 | return message.messageId 38 | } else { 39 | const error = message.status 40 | throw new Error(Object.keys(error).map((key) => `${key}: ${error[key]}`).join(', ')) 41 | } 42 | } else if (responseBody.requestError && responseBody.requestError.serviceException) { 43 | const error = responseBody.requestError.serviceException 44 | const message = Object.keys(error).map((key) => `${key}: ${error[key]}`).join(', ') 45 | throw new Error(message) 46 | } else { 47 | throw new Error(JSON.stringify(responseBody)) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/providers/sms/nexmo.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { SmsRequestType } from '../../models/notification-request' 5 | 6 | export default class SmsNexmoProvider { 7 | id: string = 'sms-nexmo-provider' 8 | credentials: Object 9 | 10 | constructor (config: Object) { 11 | this.credentials = { api_key: config.apiKey, api_secret: config.apiSecret } 12 | } 13 | 14 | /* 15 | * Note: 'nature' is not supported. 16 | */ 17 | async send (request: SmsRequestType): Promise { 18 | const { from, to, text, type, ttl, messageClass } = 19 | request.customize ? (await request.customize(this.id, request)) : request 20 | const response = await fetch('https://rest.nexmo.com/sms/json', { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 25 | }, 26 | body: JSON.stringify({ 27 | ...this.credentials, 28 | from, 29 | to, 30 | text, 31 | type, 32 | ttl, 33 | 'message-class': messageClass 34 | }) 35 | }) 36 | 37 | if (response.ok) { 38 | const responseBody = await response.json() 39 | const message = responseBody.messages[0] 40 | 41 | // Nexmo always returns 200 even for error 42 | if (message.status !== '0') { 43 | throw new Error(`status: ${message.status}, error: ${message['error-text']}`) 44 | } else { 45 | return message['message-id'] 46 | } 47 | } else { 48 | throw new Error(response.status) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/providers/sms/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotificationCatcherProvider from '../notificationCatcherProvider' 3 | // Types 4 | import type { SmsRequestType } from '../../models/notification-request' 5 | 6 | export default class SmsNotificationCatcherProvider extends NotificationCatcherProvider { 7 | async send (request: SmsRequestType): Promise { 8 | const { to, from, text } = request.customize ? (await request.customize(this.id, request)) : request 9 | return this.sendToCatcher({ 10 | to: `${to}@sms`, 11 | from, 12 | subject: `${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`, 13 | text, 14 | headers: { 15 | 'X-type': 'sms', 16 | 'X-to': `[sms] ${to}` 17 | } 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/providers/sms/ovh.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | import crypto from 'crypto' 4 | // Types 5 | import type { SmsRequestType } from '../../models/notification-request' 6 | 7 | export default class SmsOvhProvider { 8 | id: string = 'sms-ovh-provider' 9 | credentials: Object 10 | 11 | constructor ({ appKey, appSecret, consumerKey, account, host }: Object) { 12 | this.credentials = { appKey, appSecret, consumerKey, account, host } 13 | } 14 | 15 | signRequest (httpMethod: string, url: string, body: string, timestamp: number) { 16 | const { appSecret, consumerKey } = this.credentials 17 | const signature = [appSecret, consumerKey, httpMethod, url, body, timestamp] 18 | return '$1$' + crypto.createHash('sha1').update(signature.join('+')).digest('hex') 19 | } 20 | 21 | /* 22 | * Note: read this tutorial to create credentials on Ovh.com: 23 | * https://www.ovh.com/fr/g1639.envoyer_des_sms_avec_lapi_ovh_en_php 24 | */ 25 | async send (request: SmsRequestType): Promise { 26 | const { appKey, consumerKey, account, host } = this.credentials 27 | const timestamp = Math.round(Date.now() / 1000) 28 | 29 | // Documentation: https://api.ovh.com/console/#/sms/%7BserviceName%7D/jobs#POST 30 | const { from, to, text, type, ttl, messageClass } = 31 | request.customize ? (await request.customize(this.id, request)) : request 32 | 33 | const body = JSON.stringify({ 34 | sender: from, 35 | message: text, 36 | receivers: [to], 37 | charset: 'UTF-8', 38 | class: messageClass === 0 ? 'flash' 39 | : (messageClass === 1 ? 'phoneDisplay' 40 | : (messageClass === 2 ? 'sim' 41 | : (messageClass === 3 ? 'toolkit' 42 | : null))), 43 | noStopClause: type === 'transactional', 44 | validityPeriod: ttl 45 | }) 46 | 47 | // Escape unicode 48 | const reqBody = body.replace(/[\u0080-\uFFFF]/g, (m) => { 49 | return '\\u' + ('0000' + m.charCodeAt(0).toString(16)).slice(-4) 50 | }) 51 | 52 | const url = `https://${host}/1.0/sms/${account}/jobs/` 53 | 54 | const response = await fetch(url, { 55 | method: 'POST', 56 | headers: { 57 | 'X-Ovh-Timestamp': timestamp, 58 | 'X-Ovh-Signature': this.signRequest('POST', url, reqBody, timestamp), 59 | 'X-Ovh-Consumer': consumerKey, 60 | 'X-Ovh-Application': appKey, 61 | 'Content-Length': reqBody.length, 62 | 'Content-Type': 'application/json charset=utf-8', 63 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 64 | }, 65 | body 66 | }) 67 | 68 | const responseBody = await response.json() 69 | if (response.ok) { 70 | return responseBody.ids[0] 71 | } else { 72 | throw new Error(`${response.status} - ${responseBody.message}`) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/providers/sms/plivo.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { SmsRequestType } from '../../models/notification-request' 5 | 6 | export default class SmsPlivoProvider { 7 | id: string = 'sms-plivo-provider' 8 | authId: string 9 | apiKey: string 10 | 11 | constructor ({ authId, authToken }: Object) { 12 | this.authId = authId 13 | this.apiKey = Buffer.from(`${authId}:${authToken}`).toString('base64') 14 | } 15 | 16 | /* 17 | * Note: 'type', 'nature', 'ttl', 'messageClass' are not supported. 18 | */ 19 | async send (request: SmsRequestType): Promise { 20 | const { from, to, text } = request.customize ? (await request.customize(this.id, request)) : request 21 | const response = await fetch(`https://api.plivo.com/v1/Account/${this.authId}/Message/`, { 22 | method: 'POST', 23 | headers: { 24 | Authorization: `Basic ${this.apiKey}`, 25 | 'Content-Type': 'application/json', 26 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 27 | }, 28 | body: JSON.stringify({ 29 | src: from, 30 | dst: to, 31 | text 32 | }) 33 | }) 34 | 35 | if (response.ok) { 36 | const responseBody = await response.json() 37 | return responseBody.message_uuid[0] 38 | } else { 39 | throw new Error(response.status === 401 ? await response.text() : (await response.json()).error) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/providers/sms/seven.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { SmsRequestType } from '../../models/notification-request' 5 | 6 | export default class SmsSevenProvider { 7 | id: string = 'sms-seven-provider' 8 | apiKey: string 9 | 10 | constructor ({ apiKey }: Object) { 11 | this.apiKey = apiKey 12 | } 13 | 14 | /* 15 | * Note: 'nature' is not supported. 16 | */ 17 | async send (request: SmsRequestType): Promise { 18 | const { from, text, to, type, ttl, messageClass } = request.customize ? (await request.customize(this.id, request)) : request 19 | const params = { 20 | flash: messageClass === 0 ? 1 : 0, 21 | from, 22 | text, 23 | to, 24 | ttl, 25 | unicode: type === 'unicode' ? 1 : 0 26 | } 27 | const response = await fetch('https://gateway.seven.io/api/sms', { 28 | body: JSON.stringify(params), 29 | headers: { 30 | Accept: 'application/json', 31 | 'Content-Type': 'application/json', 32 | SentWith: 'Notifme', 33 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)', 34 | 'X-Api-Key': this.apiKey 35 | }, 36 | method: 'POST' 37 | }) 38 | 39 | if (response.ok) { 40 | const { messages } = await response.json() 41 | const message = messages[0] 42 | 43 | return message.id 44 | } else { 45 | throw new Error(await response.text()) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/providers/sms/twilio.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | import FormData from 'form-data' 4 | // Types 5 | import type { SmsRequestType } from '../../models/notification-request' 6 | 7 | export default class SmsTwilioProvider { 8 | id: string = 'sms-twilio-provider' 9 | accountSid: string 10 | apiKey: string 11 | 12 | constructor ({ accountSid, authToken }: Object) { 13 | this.accountSid = accountSid 14 | this.apiKey = Buffer.from(`${accountSid}:${authToken}`).toString('base64') 15 | } 16 | 17 | /* 18 | * Note: 'type', 'nature', 'messageClass' are not supported. 19 | */ 20 | async send (request: SmsRequestType): Promise { 21 | const { from, to, text, ttl } = request.customize ? (await request.customize(this.id, request)) : request 22 | const form = new FormData() 23 | form.append('From', from) 24 | form.append('To', to) 25 | form.append('Body', text) 26 | if (ttl) form.append('ValidityPeriod', ttl) 27 | const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Messages.json`, { 28 | method: 'POST', 29 | headers: { 30 | Authorization: `Basic ${this.apiKey}`, 31 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 32 | }, 33 | body: form 34 | }) 35 | 36 | const responseBody = await response.json() 37 | if (response.ok) { 38 | return responseBody.sid 39 | } else { 40 | throw new Error(`${response.status} - ${responseBody.message}`) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/providers/voice/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import VoiceLoggerProvider from '../logger' 3 | import VoiceNotificationCatcherProvider from './notificationCatcher' 4 | import VoiceTwilioProvider from './twilio' 5 | // Types 6 | import type { VoiceRequestType } from '../../models/notification-request' 7 | 8 | export interface VoiceProviderType { 9 | id: string, 10 | send(request: VoiceRequestType): Promise 11 | } 12 | 13 | export default function factory ({ type, ...config }: Object): VoiceProviderType { 14 | switch (type) { 15 | // Development 16 | case 'logger': 17 | return new VoiceLoggerProvider(config, 'voice') 18 | 19 | case 'notificationcatcher': 20 | return new VoiceNotificationCatcherProvider('voice') 21 | 22 | // Custom 23 | case 'custom': 24 | return config 25 | 26 | // Providers 27 | case 'twilio': 28 | return new VoiceTwilioProvider(config) 29 | 30 | default: 31 | throw new Error(`Unknown voice provider "${type}".`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/voice/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotificationCatcherProvider from '../notificationCatcherProvider' 3 | // Types 4 | import type { VoiceRequestType } from '../../models/notification-request' 5 | 6 | export default class VoiceNotificationCatcherProvider extends NotificationCatcherProvider { 7 | async send (request: VoiceRequestType): Promise { 8 | const { to, from, url } = request.customize ? (await request.customize(this.id, request)) : request 9 | return this.sendToCatcher({ 10 | to: `${to}@voice`, 11 | from, 12 | subject: `${to}@voice`, 13 | text: url, 14 | headers: { 15 | 'X-type': 'voice', 16 | 'X-to': `[voice] ${to}` 17 | } 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/providers/voice/twilio.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | import FormData from 'form-data' 4 | // Types 5 | import type { VoiceRequestType } from '../../models/notification-request' 6 | 7 | export default class VoiceTwilioProvider { 8 | id: string = 'voice-twilio-provider' 9 | accountSid: string 10 | apiKey: string 11 | 12 | constructor ({ accountSid, authToken }: Object) { 13 | this.accountSid = accountSid 14 | this.apiKey = Buffer.from(`${accountSid}:${authToken}`).toString('base64') 15 | } 16 | 17 | async send (request: VoiceRequestType): Promise { 18 | const { 19 | from, 20 | to, 21 | url, 22 | method, 23 | fallbackUrl, 24 | fallbackMethod, 25 | statusCallback, 26 | statusCallbackEvent, 27 | sendDigits, 28 | machineDetection, 29 | machineDetectionTimeout, 30 | timeout 31 | } = request.customize ? (await request.customize(this.id, request)) : request 32 | const form = new FormData() 33 | form.append('From', from) 34 | form.append('To', to) 35 | form.append('Url', url) 36 | if (method) form.append('Method', method) 37 | if (fallbackUrl) form.append('FallbackUrl', fallbackUrl) 38 | if (fallbackMethod) form.append('FallbackMethod', fallbackMethod) 39 | if (statusCallback) form.append('StatusCallback', statusCallback) 40 | if (statusCallbackEvent) { 41 | statusCallbackEvent.forEach((event) => form.append('StatusCallbackEvent', event)) 42 | } 43 | if (sendDigits) form.append('SendDigits', sendDigits) 44 | if (machineDetection) form.append('MachineDetection', machineDetection) 45 | if (machineDetectionTimeout) form.append('MachineDetectionTimeout', machineDetectionTimeout) 46 | if (timeout) form.append('Timeout', timeout) 47 | 48 | const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${this.accountSid}/Calls.json`, { 49 | method: 'POST', 50 | headers: { 51 | Authorization: `Basic ${this.apiKey}`, 52 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 53 | }, 54 | body: form 55 | }) 56 | 57 | const responseBody = await response.json() 58 | if (response.ok) { 59 | return responseBody.sid 60 | } else { 61 | throw new Error(`${response.status} - ${responseBody.message}`) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/providers/webpush/gcm.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import webpush from 'web-push' 3 | // Types 4 | import type { WebpushRequestType } from '../../models/notification-request' 5 | 6 | export default class WebpushGcmProvider { 7 | id: string = 'webpush-gcm-provider' 8 | options: Object 9 | 10 | constructor ({ gcmAPIKey, vapidDetails, ttl, headers }: Object) { 11 | this.options = { TTL: ttl, headers } 12 | if (gcmAPIKey) { 13 | webpush.setGCMAPIKey(gcmAPIKey) 14 | } 15 | if (vapidDetails) { 16 | const { subject, publicKey, privateKey } = vapidDetails 17 | webpush.setVapidDetails(subject, publicKey, privateKey) 18 | } 19 | } 20 | 21 | async send (request: WebpushRequestType): Promise { 22 | const { subscription, ...rest } = 23 | request.customize ? (await request.customize(this.id, request)) : request 24 | const result = await webpush.sendNotification(subscription, JSON.stringify(rest), this.options) 25 | return result.headers.location 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/providers/webpush/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import WebpushGcmProvider from './gcm' 3 | import WebpushLoggerProvider from '../logger' 4 | import WebpushNotificationCatcherProvider from './notificationCatcher' 5 | // Types 6 | import type { WebpushRequestType } from '../../models/notification-request' 7 | 8 | export interface WebpushProviderType { 9 | id: string, 10 | send(request: WebpushRequestType): Promise 11 | } 12 | 13 | export default function factory ({ type, ...config }: Object): WebpushProviderType { 14 | switch (type) { 15 | // Development 16 | case 'logger': 17 | return new WebpushLoggerProvider(config, 'webpush') 18 | 19 | case 'notificationcatcher': 20 | return new WebpushNotificationCatcherProvider('webpush') 21 | 22 | // Custom 23 | case 'custom': 24 | return config 25 | 26 | // Providers 27 | case 'gcm': 28 | return new WebpushGcmProvider(config) 29 | 30 | default: 31 | throw new Error(`Unknown webpush provider "${type}".`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/webpush/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotificationCatcherProvider from '../notificationCatcherProvider' 3 | // Types 4 | import type { WebpushRequestType } from '../../models/notification-request' 5 | 6 | export default class WebpushNotificationCatcherProvider extends NotificationCatcherProvider { 7 | async send (request: WebpushRequestType): Promise { 8 | const { subscription, title, ...rest } = 9 | request.customize ? (await request.customize(this.id, request)) : request 10 | return this.sendToCatcher({ 11 | to: `${rest.userId ? rest.userId : 'user'}@webpush`, 12 | from: '-', 13 | subject: title, 14 | headers: { 15 | 'X-type': 'webpush', 16 | 'X-to': `[webpush] ${rest.userId ? rest.userId : ''}`, 17 | 'X-payload': JSON.stringify({ title, ...rest }) 18 | } 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/providers/whatsapp/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import WhatsappInfobipProvider from './infobip' 3 | import WhatsappLoggingProvider from '../logger' 4 | import WhatsappNotificationCatcherProvider from './notificationCatcher' 5 | // Types 6 | import type { WhatsappRequestType } from '../../models/notification-request' 7 | 8 | export interface WhatsappProviderType { 9 | id: string, 10 | send(request: WhatsappRequestType): Promise 11 | } 12 | 13 | export default function factory ({ type, ...config }: Object): WhatsappProviderType { 14 | switch (type) { 15 | // Development 16 | case 'logger': 17 | return new WhatsappLoggingProvider(config, 'whatsapp') 18 | 19 | case 'notificationcatcher': 20 | return new WhatsappNotificationCatcherProvider('whatsapp') 21 | 22 | // Custom 23 | case 'custom': 24 | return config 25 | 26 | // Providers 27 | case 'infobip': 28 | return new WhatsappInfobipProvider(config) 29 | 30 | default: 31 | throw new Error(`Unknown whatsapp provider "${type}".`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/whatsapp/infobip.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from '../../util/request' 3 | // Types 4 | import type { WhatsappRequestType } from '../../models/notification-request' 5 | 6 | export default class WhatsappInfobipProvider { 7 | id: string = 'whatsapp-infobip-provider' 8 | baseUrl: string 9 | apiKey: string 10 | 11 | constructor ({ baseUrl, apiKey }: Object) { 12 | this.baseUrl = baseUrl 13 | this.apiKey = apiKey 14 | } 15 | 16 | async send (request: WhatsappRequestType): Promise { 17 | const { from, to, type, messageId, text, mediaUrl, templateName, templateData, ...rest } = request.customize ? (await request.customize(this.id, request)) : request 18 | 19 | // Construct the payload 20 | const payload = { 21 | from: (from || '').replace('+', ''), 22 | to: (to || '').replace('+', ''), 23 | messageId, 24 | content: { 25 | text, 26 | mediaUrl, 27 | templateName, 28 | templateData 29 | }, 30 | ...rest 31 | } 32 | 33 | const response = await fetch(`${this.baseUrl}/whatsapp/1/message/${type}`, { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | Authorization: `App ${this.apiKey}`, 38 | 'User-Agent': 'notifme-sdk/v1 (+https://github.com/notifme/notifme-sdk)' 39 | }, 40 | body: JSON.stringify(type === 'template' ? [payload] : payload) 41 | }) 42 | 43 | const responseBody = await response.json() 44 | if (response.ok) { 45 | // Handle the potential array or single object response 46 | const [message] = Array.isArray(responseBody.messages) ? responseBody.messages : [responseBody] 47 | return message.messageId 48 | } else { 49 | if (responseBody.requestError && responseBody.requestError.serviceException) { 50 | const error = responseBody.requestError.serviceException 51 | const message = Object.keys(error).map((key) => `${key}: ${error[key]}`).join(', ') 52 | throw new Error(message) 53 | } else { 54 | throw new Error(JSON.stringify(responseBody)) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/providers/whatsapp/notificationCatcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import NotificationCatcherProvider from '../notificationCatcherProvider' 3 | // Types 4 | import type { WhatsappRequestType } from '../../models/notification-request' 5 | 6 | export default class WhatsappCatcherProvider extends NotificationCatcherProvider { 7 | async send (request: WhatsappRequestType): Promise { 8 | const { from, to, text, ...rest } = request.customize ? (await request.customize(this.id, request)) : request 9 | this.sendToCatcher({ 10 | to: `${to}@whatsapp`, 11 | from, 12 | subject: text ? `${text.substring(0, 20)}${text.length > 20 ? '...' : ''}` : '', 13 | text: JSON.stringify({ text, ...rest }, null, 2), 14 | headers: { 15 | 'X-type': 'whatsapp', 16 | 'X-to': `[whatsapp] ${to}` 17 | } 18 | }) 19 | return '' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/sender.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import logger from './util/logger' 3 | import ProviderLogger from './providers/logger' 4 | import Registry from './util/registry' 5 | // Types 6 | import type { NotificationRequestType, NotificationStatusType, ChannelType } from './index' 7 | import type { ProvidersType } from './providers' 8 | import type { StrategiesType } from './strategies/providers' 9 | 10 | export interface SenderType { 11 | send(NotificationRequestType): Promise 12 | } 13 | 14 | export default class Sender implements SenderType { 15 | channels: string[] 16 | providers: ProvidersType 17 | strategies: StrategiesType 18 | senders: {[ChannelType]: (request: any) => Promise<{providerId: string, id: string}>} 19 | 20 | constructor (channels: string[], providers: ProvidersType, strategies: StrategiesType) { 21 | this.channels = channels 22 | this.providers = providers 23 | this.strategies = strategies 24 | 25 | // note : we can do this memoization because we do not allow to add new provider 26 | this.senders = Object.keys(strategies).reduce((acc, channel: any) => { 27 | acc[channel] = this.providers[channel].length > 0 28 | ? strategies[channel](this.providers[channel]) 29 | : async (request) => { 30 | logger.warn(`No provider registered for channel "${channel}". Using logger.`) 31 | const provider = Registry.getInstance(`${channel}-logger-default`, 32 | () => new ProviderLogger({}, channel)) 33 | 34 | return { 35 | success: true, 36 | channel, 37 | providerId: provider.id, 38 | id: await provider.send(request) 39 | } 40 | } 41 | 42 | return acc 43 | }, {}) 44 | } 45 | 46 | async send (request: NotificationRequestType): Promise { 47 | const resultsByChannel = await this.sendOnEachChannel(request) 48 | 49 | const result = resultsByChannel.reduce((acc, { success, channel, providerId, ...rest }) => ({ 50 | ...acc, 51 | channels: { 52 | ...(acc.channels || null), 53 | [channel]: { id: rest.id, providerId } 54 | }, 55 | ...(!success 56 | ? { status: 'error', errors: { ...acc.errors || null, [channel]: rest.error.message } } 57 | : null 58 | ) 59 | }), { status: 'success' }) 60 | 61 | return result 62 | } 63 | 64 | async sendOnEachChannel (request: NotificationRequestType): Promise { 65 | return Promise.all(Object.keys(request) 66 | .filter((channel) => this.channels.includes(channel)) 67 | .map(async (channel: any) => { 68 | try { 69 | return { 70 | success: true, 71 | channel, 72 | ...await this.senders[channel]({ ...request.metadata, ...request[channel] }) 73 | } 74 | } catch (error) { 75 | return { channel, success: false, error: error, providerId: error.providerId } 76 | } 77 | })) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/strategies/providers/fallback.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import logger from '../../util/logger' 3 | // Types 4 | import type { ProviderType } from '../../providers' 5 | import type { StrategyType } from './index' 6 | 7 | async function recursiveTry (providers: ProviderType[], request: any): Promise<{providerId: string, id: string}> { 8 | const [current, ...others] = providers 9 | 10 | try { 11 | const id = await current.send(request) 12 | return { providerId: current.id, id } 13 | } catch (error) { 14 | logger.warn(current.id, error) 15 | if (others.length === 0) { // no more provider to try 16 | error.providerId = current.id 17 | throw error 18 | } 19 | 20 | return recursiveTry(others, request) 21 | } 22 | } 23 | 24 | const strategyProvidersFallback: StrategyType = 25 | (providers) => (request) => recursiveTry(providers, request) 26 | 27 | export default strategyProvidersFallback 28 | -------------------------------------------------------------------------------- /src/strategies/providers/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import strategyFallback from './fallback' 3 | import strategyNoFallback from './no-fallback' 4 | import strategyRoundrobin from './roundrobin' 5 | // Types 6 | import type { ProviderType, ChannelOptionsType } from '../../providers' 7 | import type { ChannelType } from '../../index' 8 | 9 | export type StrategyType = (providers: ProviderType[]) => (request: any) => Promise<{ 10 | providerId: string, 11 | id: string 12 | }> 13 | export type StrategiesType = {[ChannelType]: StrategyType} 14 | 15 | const providerStrategies = { 16 | fallback: strategyFallback, 17 | 'no-fallback': strategyNoFallback, 18 | roundrobin: strategyRoundrobin 19 | } 20 | 21 | const strategies = Object.keys(providerStrategies) 22 | 23 | export default function factory (channels: ChannelOptionsType): StrategiesType { 24 | return Object.keys(channels).reduce((acc, key: any): StrategiesType => { 25 | const optionStrategy = (channels[key]: any).multiProviderStrategy 26 | if (typeof optionStrategy === 'function') { 27 | acc[key] = optionStrategy 28 | } else if (strategies.includes(optionStrategy)) { 29 | acc[key] = providerStrategies[optionStrategy] 30 | } else { 31 | throw new Error(`"${optionStrategy}" is not a valid strategy. Strategy must be a function or ${strategies.join('|')}.`) 32 | } 33 | return acc 34 | }, {}) 35 | } 36 | -------------------------------------------------------------------------------- /src/strategies/providers/no-fallback.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import logger from '../../util/logger' 3 | // Types 4 | import type { StrategyType } from './index' 5 | 6 | const strategyProvidersNoFallback: StrategyType = 7 | ([provider]) => async (request) => { 8 | try { 9 | const id = await provider.send(request) 10 | return { providerId: provider.id, id } 11 | } catch (error) { 12 | logger.warn(provider.id, error) 13 | error.providerId = provider.id 14 | throw error 15 | } 16 | } 17 | 18 | export default strategyProvidersNoFallback 19 | -------------------------------------------------------------------------------- /src/strategies/providers/roundrobin.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // "statefull" strategy 3 | import strategyFallback from './fallback' 4 | // Types 5 | import type { StrategyType } from './index' 6 | 7 | function rotate (arr: T[], forward): T[] { // /!\ mute array, the mutation is "the state" 8 | if (forward) { 9 | arr.push(arr.shift()) 10 | } else { 11 | arr.unshift(arr.pop()) 12 | } 13 | return arr 14 | } 15 | 16 | const strategyProvidersFallback: StrategyType = (providers) => { 17 | const rotatedProviders = rotate(providers, false) 18 | return (request) => strategyFallback(rotate(rotatedProviders, true))(request) 19 | } 20 | 21 | // /!\ not equivalent to (providers) => strategyFallback(rotate(providers)) because of memoization 22 | 23 | export default strategyProvidersFallback 24 | -------------------------------------------------------------------------------- /src/util/aws/v4_credentials.js: -------------------------------------------------------------------------------- 1 | /* https://github.com/aws/aws-sdk-js/blob/master/lib/signers/v4_credentials.js */ 2 | import { hmac } from '../crypto' 3 | 4 | /** 5 | * @api private 6 | */ 7 | var cachedSecret = {} 8 | 9 | /** 10 | * @api private 11 | */ 12 | var cacheQueue = [] 13 | 14 | /** 15 | * @api private 16 | */ 17 | var maxCacheEntries = 50 18 | 19 | /** 20 | * @api private 21 | */ 22 | var v4Identifier = 'aws4_request' 23 | 24 | export default { 25 | /** 26 | * @api private 27 | * 28 | * @param date [String] 29 | * @param region [String] 30 | * @param serviceName [String] 31 | * @return [String] 32 | */ 33 | createScope: function createScope (date, region, serviceName) { 34 | return [ 35 | date.substr(0, 8), 36 | region, 37 | serviceName, 38 | v4Identifier 39 | ].join('/') 40 | }, 41 | 42 | /** 43 | * @api private 44 | * 45 | * @param credentials [Credentials] 46 | * @param date [String] 47 | * @param region [String] 48 | * @param service [String] 49 | * @param shouldCache [Boolean] 50 | * @return [String] 51 | */ 52 | getSigningKey: function getSigningKey ( 53 | credentials, 54 | date, 55 | region, 56 | service, 57 | shouldCache 58 | ) { 59 | var credsIdentifier = hmac(credentials.secretAccessKey, credentials.accessKeyId, 'base64') 60 | var cacheKey = [credsIdentifier, date, region, service].join('_') 61 | shouldCache = shouldCache !== false 62 | if (shouldCache && (cacheKey in cachedSecret)) { 63 | return cachedSecret[cacheKey] 64 | } 65 | 66 | var kDate = hmac( 67 | 'AWS4' + credentials.secretAccessKey, 68 | date, 69 | 'buffer' 70 | ) 71 | var kRegion = hmac(kDate, region, 'buffer') 72 | var kService = hmac(kRegion, service, 'buffer') 73 | 74 | var signingKey = hmac(kService, v4Identifier, 'buffer') 75 | if (shouldCache) { 76 | cachedSecret[cacheKey] = signingKey 77 | cacheQueue.push(cacheKey) 78 | if (cacheQueue.length > maxCacheEntries) { 79 | // remove the oldest entry (not the least recently used) 80 | delete cachedSecret[cacheQueue.shift()] 81 | } 82 | } 83 | 84 | return signingKey 85 | }, 86 | 87 | /** 88 | * @api private 89 | * 90 | * Empties the derived signing key cache. Made available for testing purposes 91 | * only. 92 | */ 93 | emptyCache: function emptyCache () { 94 | cachedSecret = {} 95 | cacheQueue = [] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/util/crypto.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import crypto from 'crypto' 3 | 4 | export function hmac (key: string | Buffer, data: string, encoding: 'hex' | 'latin1' | 'base64' | 'buffer') { 5 | return crypto.createHmac('sha256', key) 6 | .update(typeof data === 'string' ? Buffer.from(data) : data) 7 | .digest(encoding === 'buffer' ? undefined : encoding) 8 | } 9 | 10 | export function sha256 (data: string, encoding: 'hex' | 'latin1' | 'base64' | 'buffer') { 11 | return crypto.createHash('sha256') 12 | .update(typeof data === 'string' ? Buffer.from(data) : data) 13 | .digest(encoding === 'buffer' ? undefined : encoding) 14 | } 15 | -------------------------------------------------------------------------------- /src/util/dedupe.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export default function dedupe (array: Array): Array { 4 | return array.filter((element, position) => array.indexOf(element) === position) 5 | } 6 | -------------------------------------------------------------------------------- /src/util/logger.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import winston from 'winston' 3 | 4 | export type LevelType = 'error' | 'warn' | 'info' 5 | 6 | class Logger { 7 | innerLogger: winston 8 | 9 | constructor () { 10 | this.innerLogger = winston.createLogger() 11 | this.configure({ 12 | transports: [ 13 | new winston.transports.Console({ colorize: true }) 14 | ] 15 | }) 16 | } 17 | 18 | configure (options: Object) { 19 | this.innerLogger.configure(options) 20 | } 21 | 22 | mute () { 23 | this.configure({ transports: [] }) 24 | } 25 | 26 | log (level: LevelType, info: any, extra?: any) { 27 | this.innerLogger.log(level, info, extra) 28 | } 29 | 30 | error (info: any, extra?: any) { 31 | this.log('error', info, extra) 32 | } 33 | 34 | warn (info: any, extra?: any) { 35 | this.log('warn', info, extra) 36 | } 37 | 38 | info (info: any, extra?: any) { 39 | this.log('info', info, extra) 40 | } 41 | } 42 | 43 | export default new Logger() 44 | -------------------------------------------------------------------------------- /src/util/registry.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | class Registry { 4 | map: {[key: string]: any} = {} 5 | 6 | getInstance (key: string, getValueIfUndefined: () => T): T { 7 | if (!this.map[key]) { 8 | this.map[key] = getValueIfUndefined() 9 | } 10 | return this.map[key] 11 | } 12 | } 13 | 14 | export default new Registry() 15 | -------------------------------------------------------------------------------- /src/util/request.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import fetch from 'node-fetch' 3 | import HttpsProxyAgent from 'https-proxy-agent' 4 | 5 | export default (url: string, { ...options }: Object = {}) => { 6 | if (!options.agent && process.env.NOTIFME_HTTP_PROXY) { 7 | options.agent = new HttpsProxyAgent(process.env.NOTIFME_HTTP_PROXY) 8 | } 9 | 10 | return fetch(url, options) 11 | } 12 | --------------------------------------------------------------------------------