├── .npmignore ├── test ├── mocha.opts └── unit │ ├── index.js │ ├── BaseNotification.test.js │ ├── APNNotification.test.js │ └── GCMNotification.test.js ├── .babelrc ├── .editorconfig ├── .snyk ├── src ├── index.js ├── BaseNotification.js ├── GCMNotification.js └── APNNotification.js ├── .travis.yml ├── .gitignore ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | src 3 | test 4 | .idea 5 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | ./test/unit/**/*.test.js 2 | --reporter nyan 3 | --recursive 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - lodash: 8 | patched: '2020-05-01T03:18:49.364Z' 9 | - node-gcm > lodash: 10 | patched: '2020-05-01T03:18:49.364Z' 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import APNNotification from './APNNotification'; 2 | import GCMNotification from './GCMNotification'; 3 | 4 | const pusher = { 5 | ios: APNNotification, 6 | android: GCMNotification 7 | }; 8 | 9 | /** 10 | * Create instance of Pusher service 11 | * @param {String} type 12 | * @param {Object} [config] 13 | * @returns {*} 14 | */ 15 | export default function (type, config) { 16 | if (pusher[type.toLowerCase()] instanceof Function) { 17 | return new pusher[type.toLowerCase()](config); 18 | } else { 19 | throw new Error('Unrecognized type -> ' + type); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: true 8 | node_js: 9 | - stable 10 | - 5 11 | - 4 12 | - 0.12 13 | - 0.10 14 | before_install: 15 | - npm install -g npm@latest 16 | before_script: 17 | - npm prune 18 | after_success: 19 | - 'curl -Lo travis_after_all.py https://git.io/travis_after_all' 20 | - python travis_after_all.py 21 | - 'export $(cat .to_export_back) &> /dev/null' 22 | - npm run coveralls 23 | - npm run semantic-release 24 | branches: 25 | except: 26 | - "/^v\\d+\\.\\d+\\.\\d+$/" 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | node_modules 27 | 28 | # Babel compiled sources 29 | lib 30 | 31 | # Miscellaneous 32 | *~ 33 | *# 34 | .DS_STORE 35 | .netbeans 36 | nbproject 37 | .idea 38 | .node_history 39 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import PusherService from '../../src/index'; 3 | import APNNotification from '../../src/APNNotification'; 4 | import GCMNotification from '../../src/GCMNotification'; 5 | 6 | describe('PusherService', () => { 7 | it('Should properly export', () => { 8 | assert.isFunction(PusherService); 9 | }); 10 | 11 | it('Should properly create ios instance', () => { 12 | assert.instanceOf(PusherService('ios'), APNNotification); 13 | }); 14 | 15 | it('Should properly create android instance', () => { 16 | assert.instanceOf(PusherService('android'), GCMNotification); 17 | }); 18 | 19 | it('Should properly throw exception on create unrecognized', () => { 20 | assert.throw(() => PusherService('NOT_EXISTS'), Error); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/BaseNotification.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default class BaseNotification { 4 | constructor(_config) { 5 | this._config = {}; 6 | this._provider = {}; 7 | 8 | _.assign(this._config, _config); 9 | } 10 | 11 | /** 12 | * Get configuration value 13 | * @param {String} [path] 14 | * @returns {*} 15 | */ 16 | get(path) { 17 | return typeof path === 'undefined' ? this._config : _.get(this._config, path); 18 | } 19 | 20 | /** 21 | * Set configuration value 22 | * @param {String} path 23 | * @param {*} value 24 | * @returns {BaseNotification} 25 | */ 26 | set(path, value) { 27 | _.set(this._config, path, value); 28 | return this; 29 | } 30 | 31 | /** 32 | * Get provider for sending notifications 33 | * @returns {*} 34 | */ 35 | getProvider() { 36 | return this._provider; 37 | } 38 | 39 | /** 40 | * Set new provider to this pusher 41 | * @param {*} provider 42 | * @returns {BaseNotification} 43 | */ 44 | setProvider(provider) { 45 | this._provider = provider; 46 | return this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Eugene Obrezkov 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 | -------------------------------------------------------------------------------- /test/unit/BaseNotification.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import BaseNotification from '../../src/BaseNotification'; 3 | 4 | describe('BaseNotification', () => { 5 | it('Should properly export', () => { 6 | assert.isFunction(BaseNotification); 7 | }); 8 | 9 | it('Should properly make objects configurable', () => { 10 | let notification = new BaseNotification(); 11 | 12 | assert.notOk(notification.get('foo')); 13 | assert.instanceOf(notification.set('foo', 'bar'), BaseNotification); 14 | assert.instanceOf(notification.set('obj', {foo: 'bar'}), BaseNotification); 15 | assert.deepEqual(notification.get(), {foo: 'bar', obj: {foo: 'bar'}}); 16 | assert.deepEqual(notification.get('obj'), {foo: 'bar'}); 17 | assert.equal(notification.get('obj.foo'), 'bar'); 18 | assert.equal(notification.get('foo'), 'bar'); 19 | }); 20 | 21 | it('Should properly create notification with pre-defined config', () => { 22 | let notification = new BaseNotification({ 23 | foo: 'bar', 24 | obj: { 25 | foo: 'bar' 26 | } 27 | }); 28 | 29 | assert.equal(notification.get('foo'), 'bar'); 30 | assert.equal(notification.get('obj.foo'), 'bar'); 31 | assert.deepEqual(notification.get('obj'), {foo: 'bar'}); 32 | assert.notOk(notification.get('NOT_EXISTS')); 33 | }); 34 | 35 | it('Should properly get/set provider', () => { 36 | let notification = new BaseNotification(); 37 | 38 | assert.deepEqual(notification.getProvider(), {}); 39 | assert.instanceOf(notification.setProvider('NOTIFICATION'), BaseNotification); 40 | assert.equal(notification.getProvider(), 'NOTIFICATION'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-service-pusher", 3 | "version": "0.0.0-semantic-release", 4 | "description": "Service for Sails framework with Pusher features", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "compile": "babel src --out-dir lib", 8 | "coveralls": "cat coverage/lcov.info | coveralls", 9 | "prepublish": "npm run snyk-protect && npm run compile", 10 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 11 | "test": "babel-node ./node_modules/.bin/isparta cover _mocha", 12 | "snyk-protect": "snyk protect" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/ghaiklor/sails-service-pusher.git" 17 | }, 18 | "keywords": [ 19 | "sails", 20 | "service", 21 | "pusher" 22 | ], 23 | "author": "ghaiklor", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/ghaiklor/sails-service-pusher/issues" 27 | }, 28 | "homepage": "https://github.com/ghaiklor/sails-service-pusher#readme", 29 | "dependencies": { 30 | "apn": "3.0.0-alpha1", 31 | "lodash": "4.17.20", 32 | "node-gcm": "1.0.4", 33 | "snyk": "^1.316.1" 34 | }, 35 | "devDependencies": { 36 | "babel-cli": "6.26.0", 37 | "babel-plugin-add-module-exports": "1.0.1", 38 | "babel-preset-es2015": "6.24.1", 39 | "chai": "4.1.2", 40 | "coveralls": "3.0.8", 41 | "cz-conventional-changelog": "3.1.0", 42 | "isparta": "4.1.1", 43 | "mocha": "7.1.0", 44 | "semantic-release": "15.14.0", 45 | "sinon": "7.4.0" 46 | }, 47 | "config": { 48 | "commitizen": { 49 | "path": "./node_modules/cz-conventional-changelog" 50 | } 51 | }, 52 | "publishConfig": { 53 | "tag": "latest" 54 | }, 55 | "release": { 56 | "branch": "master" 57 | }, 58 | "snyk": true 59 | } 60 | -------------------------------------------------------------------------------- /src/GCMNotification.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import gcm from 'node-gcm'; 3 | import BaseNotification from './BaseNotification'; 4 | 5 | export default class GCMNotification extends BaseNotification { 6 | constructor(config) { 7 | super(config); 8 | 9 | this.setProvider(new gcm.Sender(this.get('provider.apiKey'), this.get('provider'))); 10 | } 11 | 12 | /** 13 | * Create new message object 14 | * @param {Object} [_notification] Notification object 15 | * @param {Object} [_config] Additional configuration object 16 | * @private 17 | */ 18 | createNotification(_notification, _config) { 19 | let predefinedNotification = this.get('notification') || {}; 20 | let customNotification = _notification || {}; 21 | let config = _.merge({ 22 | data: customNotification.payload || predefinedNotification.payload || {}, 23 | notification: { 24 | title: customNotification.title || predefinedNotification.title, 25 | body: customNotification.body || predefinedNotification.body, 26 | icon: customNotification.icon || predefinedNotification.icon, 27 | sound: customNotification.sound || predefinedNotification.sound, 28 | badge: customNotification.badge || predefinedNotification.badge 29 | } 30 | }, _config); 31 | 32 | return new gcm.Message(config); 33 | } 34 | 35 | /** 36 | * Send notification to device 37 | * @param {Array} [_device] Array with device tokens or string with token 38 | * @param {Object} [_notification] Notification configuration object 39 | * @param {Object} [_config] Additional configuration for notification 40 | * @returns {Promise} 41 | * APNNotification.send('DEVICE', { 42 | * title: 'Notification title', 43 | * body: 'Notification body text', 44 | * icon: 'Drawable resource', 45 | * sound: 'Sound to be played', 46 | * badge: 'The badge on client app', 47 | * payload: {} 48 | * }); 49 | */ 50 | send(_device, _notification, _config) { 51 | let device = [].concat(this.get('device') || []).concat(_device || []); 52 | let message = this.createNotification(_notification, _config); 53 | 54 | return new Promise((resolve, reject) => { 55 | this.getProvider().send(message, device, (error, result) => error ? reject(error) : resolve(result)); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/APNNotification.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import apn from 'apn'; 3 | import BaseNotification from './BaseNotification'; 4 | 5 | /** 6 | * Default stderr 7 | * @type {Function} 8 | * @private 9 | */ 10 | const DEFAULT_STDERR = console.error.bind(console); 11 | 12 | export default class APNNotification extends BaseNotification { 13 | constructor(config) { 14 | super(config); 15 | 16 | this.setProvider(new apn.Connection(this.get('provider'))); 17 | 18 | this 19 | .getProvider() 20 | .on('error', this.get('stderr') || DEFAULT_STDERR) 21 | .on('transmissionError', this.get('stderr') || DEFAULT_STDERR); 22 | } 23 | 24 | /** 25 | * Create apn device with specified token 26 | * @param {String} _token Device token 27 | * @returns {Device} 28 | * @private 29 | */ 30 | createDevice(_token) { 31 | return new apn.Device(_token); 32 | } 33 | 34 | /** 35 | * Create apn notification 36 | * @param {Object} _notification Configuration for the notification 37 | * @param {Object} _config Additional configuration for the notification object 38 | * @returns {Notification} 39 | * @private 40 | */ 41 | createNotification(_notification, _config) { 42 | let predefinedNotification = this.get('notification') || {}; 43 | let customNotification = _notification || {}; 44 | let notification = new apn.Notification(customNotification.payload || predefinedNotification.payload || {}); 45 | 46 | notification.sound = customNotification.sound || predefinedNotification.sound; 47 | notification.badge = customNotification.badge || predefinedNotification.badge; 48 | notification.setAlertTitle(customNotification.title || predefinedNotification.title); 49 | notification.setAlertText(customNotification.body || predefinedNotification.body); 50 | 51 | return _.merge(notification, _config); 52 | } 53 | 54 | /** 55 | * Send push notification for one of devices 56 | * @param {Device} _device Device object 57 | * @param {Notification} _notification Notification object 58 | * @returns {APNNotification} 59 | * @private 60 | */ 61 | sendToDevice(_device, _notification) { 62 | this.getProvider().pushNotification(_notification, _device); 63 | return this; 64 | } 65 | 66 | /** 67 | * Send push notification to devices 68 | * @param {Array} [_device] Device tokens in array of string or string 69 | * @param {Object} [_notification] Notification configuration 70 | * @param {Object} [_config] Additional configuration for notification 71 | * @returns {APNNotification} 72 | * @example 73 | * APNNotification.send(['DEVICE'], { 74 | * title: 'Notification title', 75 | * body: 'Notification body text', 76 | * icon: 'Drawable resource', 77 | * sound: 'Sound to be played', 78 | * badge: 'The badge on client app', 79 | * payload: {} 80 | * }); 81 | */ 82 | send(_device, _notification, _config) { 83 | let device = [].concat(this.get('device') || []).concat(_device || []); 84 | let notification = this.createNotification(_notification, _config); 85 | 86 | for (let i = 0; i < device.length; i++) { 87 | this.sendToDevice(this.createDevice(device[i]), notification); 88 | } 89 | 90 | return Promise.resolve(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/unit/APNNotification.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import sinon from 'sinon'; 3 | import APNNotification from '../../src/APNNotification'; 4 | 5 | const CONFIG = { 6 | device: ['a1'], 7 | provider: { 8 | cert: 'cert.pem', 9 | key: 'key.pem', 10 | production: false 11 | }, 12 | notification: { 13 | title: 'TITLE', 14 | body: 'BODY', 15 | icon: 'ICON', 16 | sound: 'SOUND', 17 | badge: 'BADGE', 18 | payload: { 19 | foo: 'bar', 20 | bar: 'foo' 21 | } 22 | } 23 | }; 24 | 25 | const NOTIFICATION_SHOULD_BE = { 26 | encoding: 'utf8', 27 | payload: {foo: 'bar', bar: 'foo'}, 28 | expiry: 0, 29 | priority: 10, 30 | retryLimit: -1, 31 | device: undefined, 32 | compiled: false, 33 | truncateAtWordEnd: false, 34 | _sound: 'SOUND', 35 | _alert: {title: 'TITLE', body: 'BODY'} 36 | }; 37 | 38 | describe('APNNotification', () => { 39 | it('Should properly export', () => { 40 | assert.isFunction(APNNotification); 41 | }); 42 | 43 | it('Should properly send notification with pre-defined options', done => { 44 | let ios = new APNNotification(CONFIG); 45 | 46 | sinon.stub(ios.getProvider(), 'pushNotification'); 47 | 48 | ios 49 | .send(['b2', 'c3']) 50 | .then(() => { 51 | assert.ok(ios.getProvider().pushNotification.calledThrice); 52 | assert.deepEqual(ios.getProvider().pushNotification.getCall(0).args[0].payload, {foo: 'bar', bar: 'foo'}); 53 | assert.deepEqual(ios.getProvider().pushNotification.getCall(0).args[0]._sound, 'SOUND'); 54 | assert.deepEqual(ios.getProvider().pushNotification.getCall(0).args[0]._alert, {title: 'TITLE', body: 'BODY'}); 55 | assert.equal(ios.getProvider().pushNotification.getCall(0).args[1], 'a1'); 56 | assert.equal(ios.getProvider().pushNotification.getCall(1).args[1], 'b2'); 57 | assert.equal(ios.getProvider().pushNotification.getCall(2).args[1], 'c3'); 58 | 59 | ios.getProvider().pushNotification.restore(); 60 | 61 | done(); 62 | }) 63 | .catch(done); 64 | }); 65 | 66 | it('Should properly send notification with custom notification', done => { 67 | let ios = new APNNotification(CONFIG); 68 | 69 | sinon.stub(ios.getProvider(), 'pushNotification'); 70 | 71 | ios 72 | .send(['b2', 'c3'], { 73 | body: 'OVERRIDE_BODY' 74 | }) 75 | .then(() => { 76 | assert.ok(ios.getProvider().pushNotification.calledThrice); 77 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], '_alert.body', 'OVERRIDE_BODY'); 78 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], '_sound', 'SOUND'); 79 | assert.equal(ios.getProvider().pushNotification.getCall(0).args[1], 'a1'); 80 | assert.equal(ios.getProvider().pushNotification.getCall(1).args[1], 'b2'); 81 | assert.equal(ios.getProvider().pushNotification.getCall(2).args[1], 'c3'); 82 | 83 | ios.getProvider().pushNotification.restore(); 84 | 85 | done(); 86 | }) 87 | .catch(done); 88 | }); 89 | 90 | it('Should properly send notification with extended notification', done => { 91 | let ios = new APNNotification(CONFIG); 92 | 93 | sinon.stub(ios.getProvider(), 'pushNotification'); 94 | 95 | ios 96 | .send(['b2'], { 97 | body: 'OVERRIDE_BODY' 98 | }, { 99 | priority: 5 100 | }) 101 | .then(() => { 102 | assert.ok(ios.getProvider().pushNotification.calledTwice); 103 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], '_alert.body', 'OVERRIDE_BODY'); 104 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], 'priority', 5); 105 | assert.equal(ios.getProvider().pushNotification.getCall(0).args[1], 'a1'); 106 | assert.equal(ios.getProvider().pushNotification.getCall(1).args[1], 'b2'); 107 | 108 | ios.getProvider().pushNotification.restore(); 109 | 110 | done(); 111 | }) 112 | .catch(done); 113 | }); 114 | 115 | it('Should properly send notification with all empty config', done => { 116 | let ios = new APNNotification(); 117 | 118 | sinon.stub(ios.getProvider(), 'pushNotification'); 119 | 120 | ios 121 | .send() 122 | .then(() => { 123 | assert.ok(ios.getProvider().pushNotification.notCalled); 124 | ios.getProvider().pushNotification.restore(); 125 | 126 | done(); 127 | }) 128 | .catch(done); 129 | }); 130 | 131 | it('Should properly send notification with empty pre-defined config and empty notification', done => { 132 | let ios = new APNNotification(); 133 | 134 | sinon.stub(ios.getProvider(), 'pushNotification'); 135 | 136 | ios 137 | .send(['a1']) 138 | .then(() => { 139 | assert.ok(ios.getProvider().pushNotification.calledOnce); 140 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], '_alert.title', undefined); 141 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], '_alert.body', undefined); 142 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], 'priority', 10); 143 | assert.equal(ios.getProvider().pushNotification.getCall(0).args[1], 'a1'); 144 | 145 | ios.getProvider().pushNotification.restore(); 146 | 147 | done(); 148 | }) 149 | .catch(done); 150 | }); 151 | 152 | it('Should properly send notification with empty pre-defined config and custom notification', done => { 153 | let ios = new APNNotification(); 154 | 155 | sinon.stub(ios.getProvider(), 'pushNotification'); 156 | 157 | ios 158 | .send(['a1'], { 159 | title: 'CUSTOM_TITLE', 160 | body: 'CUSTOM_BODY' 161 | }, { 162 | priority: 5 163 | }) 164 | .then(() => { 165 | assert.ok(ios.getProvider().pushNotification.calledOnce); 166 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], '_alert.title', 'CUSTOM_TITLE'); 167 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], '_alert.body', 'CUSTOM_BODY'); 168 | assert.deepPropertyVal(ios.getProvider().pushNotification.getCall(0).args[0], 'priority', 5); 169 | assert.equal(ios.getProvider().pushNotification.getCall(0).args[1], 'a1'); 170 | 171 | ios.getProvider().pushNotification.restore(); 172 | 173 | done(); 174 | }) 175 | .catch(done); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sails-service-pusher 2 | 3 | ![Build Status](https://img.shields.io/travis/ghaiklor/sails-service-pusher.svg) 4 | ![Coverage](https://img.shields.io/coveralls/ghaiklor/sails-service-pusher.svg) 5 | 6 | ![Downloads](https://img.shields.io/npm/dm/sails-service-pusher.svg) 7 | ![Downloads](https://img.shields.io/npm/dt/sails-service-pusher.svg) 8 | ![npm version](https://img.shields.io/npm/v/sails-service-pusher.svg) 9 | ![License](https://img.shields.io/npm/l/sails-service-pusher.svg) 10 | 11 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 12 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 13 | ![dependencies](https://img.shields.io/david/ghaiklor/sails-service-pusher.svg) 14 | ![dev dependencies](https://img.shields.io/david/dev/ghaiklor/sails-service-pusher.svg) 15 | 16 | Service for Sails framework with Pusher features. 17 | 18 | ## List of supported pusher services 19 | 20 | - APNS (Apple Push Notification Service) 21 | - GCMS (Google Cloud Messaging Service) 22 | 23 | ## Getting Started 24 | 25 | Install this module. 26 | 27 | ```shell 28 | npm install sails-service-pusher 29 | ``` 30 | 31 | Then require it in your service and create pusher instance. 32 | 33 | ```javascript 34 | // api/services/PusherService.js 35 | import PusherService from 'sails-service-pusher'; 36 | 37 | export default PusherService('ios', { 38 | provider: { 39 | cert: 'cert.pem', 40 | key: 'key.pem', 41 | production: false 42 | } 43 | }); 44 | 45 | // api/controllers/PusherController.js 46 | export default { 47 | send: function(req, res) { 48 | PusherService 49 | .send(['DEVICE_TOKEN_1', 'DEVICE_TOKEN_2'], { 50 | title: req.param('title') || 'Pusher', 51 | body: req.param('body') || 'Hello from sails-service-pusher' 52 | }) 53 | .then(res.ok) 54 | .catch(res.negotiate); 55 | } 56 | }; 57 | ``` 58 | 59 | ## Configuration 60 | 61 | When you instantiate new instance via `PusherService()` you can provide configuration object with 3 keys: 62 | 63 | - `config.device` - {Array} Device tokens that should get notification (will be merged with another devices in `send()`) 64 | - `config.provider` - {Object} Options that will go to each of SDKs ([APN](https://github.com/argon/node-apn/blob/master/doc/connection.markdown#apnconnectionoptions), [GCM](https://github.com/ToothlessGear/node-gcm#example-application)) 65 | - `config.notification` - {Object} Options that will go to each of notifications (it has one interface for each of providers, see below) 66 | 67 | ## API 68 | 69 | Each of Pusher instances has only one method: 70 | 71 | ### send([device], [notification], [config]) 72 | 73 | Sends Push Notification. 74 | 75 | `device` - {Array} Device tokens (registration IDs) to which push need to send (mixed up with pre-defined devices). 76 | 77 | `notification` - {Object} Config for notification: 78 | 79 | - `notification.title` - Notification title 80 | - `notification.body` - Notification body text 81 | - `notification.icon` - Notification icon 82 | - `notification.sound` - Notification sound to be played 83 | - `notification.badge` - Indicates the badge on client app home icon 84 | - `notification.payload` - Custom data to send within Push Notification 85 | 86 | `config` - Additional configuration for notification with specific platform. See appropriate documentation. 87 | 88 | ## Examples 89 | 90 | ### APNNotification 91 | 92 | All of this examples contains all the configuration keys. And most of them is optional. 93 | 94 | ```javascript 95 | let ios = PusherService('ios', { 96 | device: [], // Array of string with device tokens 97 | provider: { 98 | cert: 'cert.pem', // The filename of the connection certificate to load from disk 99 | key: 'key.pem', // The filename of the connection key to load from disk 100 | ca: [], // An array of trusted certificates 101 | pfx: '', // File path for private key, certificate and CA certs in PFX or PKCS12 format 102 | passphrase: '', // The passphrase for the connection key 103 | production: false, // Specifies which environment to connect to: Production (if true) or Sandbox 104 | voip: false, // Enable when you are using a VoIP certificate to enable paylods up to 4096 bytes 105 | port: 2195, // Gateway port 106 | rejectUnauthorized: true, // Reject Unauthorized property to be passed through to tls.connect() 107 | cacheLength: 1000, // Number of notifications to cache for error purposes 108 | autoAdjustCache: true, // Whether the cache should grow in response to messages being lost after errors 109 | maxConnections: 1, // The maximum number of connections to create for sending messages 110 | connectTimeout: 10000, // The duration of time the module should wait, in milliseconds 111 | connectionTimeout: 3600000, // The duration the socket should stay alive with no activity in milliseconds 112 | connectionRetryLimit: 10, // The maximum number of connection failures that will be tolerated before apn will "terminate" 113 | buffersNotifications: true, // Whether to buffer notifications and resend them after failure 114 | fastMode: false // Whether to aggresively empty the notification buffer while connected 115 | }, 116 | notification: { 117 | title: 'iOS Test Push', // Indicates notification title 118 | body: 'Hey, there!', // Indicates notification body text 119 | icon: '', // Indicates notification icon 120 | sound: '', // Indicates sound to be played 121 | badge: '', // Indicates the badge on client app home icon 122 | payload: {} // Custom data to send within Push Notification 123 | } 124 | }); 125 | 126 | ios 127 | .send(['TOKEN_1', 'TOKEN_2'], { 128 | body: 'You can override pre-defined' 129 | }) 130 | .then(console.log.bind(console)) 131 | .catch(console.error.bind(console)); 132 | ``` 133 | 134 | ### GCMNotification 135 | 136 | ```javascript 137 | let android = PusherService('android', { 138 | device: [], // Array of string with device tokens 139 | provider: { 140 | apiKey: '', // Your Google Server API Key 141 | maxSockets: 12, // Max number of sockets to have open at one time 142 | proxy: 'http://your-proxy.com' // This is [just like passing a proxy on to request](https://github.com/request/request#proxies) 143 | }, 144 | notification: { 145 | title: 'Android Test Push', // Indicates notification title 146 | body: 'Hey, there!', // Indicates notification body text 147 | icon: '', // Indicates notification icon 148 | sound: '', // Indicates sound to be played 149 | badge: '', // Indicates the badge on client app home icon 150 | payload: {} // Custom data to send within Push Notification 151 | } 152 | }); 153 | 154 | android 155 | .send(['TOKEN_1', 'TOKEN_2'], { 156 | body: 'You can override pre-defined' 157 | }) 158 | .then(console.log.bind(console)) 159 | .catch(console.error.bind(console)); 160 | ``` 161 | 162 | ## License 163 | 164 | The MIT License (MIT) 165 | 166 | Copyright (c) 2015 Eugene Obrezkov 167 | 168 | Permission is hereby granted, free of charge, to any person obtaining a copy 169 | of this software and associated documentation files (the "Software"), to deal 170 | in the Software without restriction, including without limitation the rights 171 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 172 | copies of the Software, and to permit persons to whom the Software is 173 | furnished to do so, subject to the following conditions: 174 | 175 | The above copyright notice and this permission notice shall be included in all 176 | copies or substantial portions of the Software. 177 | 178 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 179 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 180 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 181 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 182 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 183 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 184 | SOFTWARE. 185 | -------------------------------------------------------------------------------- /test/unit/GCMNotification.test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import sinon from 'sinon'; 3 | import GCMNotification from '../../src/GCMNotification'; 4 | 5 | const CONFIG = { 6 | device: ['a1'], 7 | provider: { 8 | apiKey: 'test' 9 | }, 10 | notification: { 11 | title: 'TITLE', 12 | body: 'BODY', 13 | icon: 'ICON', 14 | sound: 'SOUND', 15 | badge: 'BADGE', 16 | payload: { 17 | foo: 'bar', 18 | bar: 'foo' 19 | } 20 | } 21 | }; 22 | 23 | const NOTIFICATION_SHOULD_BE = { 24 | notification: { 25 | title: 'TITLE', 26 | body: 'BODY', 27 | icon: 'ICON', 28 | sound: 'SOUND', 29 | badge: 'BADGE' 30 | }, 31 | data: { 32 | foo: 'bar', 33 | bar: 'foo' 34 | } 35 | }; 36 | 37 | describe('GCMNotification', () => { 38 | it('Should properly export', () => { 39 | assert.isFunction(GCMNotification); 40 | }); 41 | 42 | it('Should properly send notification with pre-defined options', done => { 43 | let android = new GCMNotification(CONFIG); 44 | 45 | sinon.stub(android.getProvider(), 'send', (message, devices, cb) => cb(null, 'RESULT')); 46 | 47 | android 48 | .send(['b2', 'c3']) 49 | .then(result => { 50 | assert.equal(result, 'RESULT'); 51 | assert(android.getProvider().send.calledOnce); 52 | assert.deepEqual(android.getProvider().send.getCall(0).args[0].params, NOTIFICATION_SHOULD_BE); 53 | assert.deepEqual(android.getProvider().send.getCall(0).args[1], ['a1', 'b2', 'c3']); 54 | assert.isFunction(android.getProvider().send.getCall(0).args[2]); 55 | 56 | android.getProvider().send.restore(); 57 | 58 | done(); 59 | }) 60 | .catch(done); 61 | }); 62 | 63 | it('Should properly send notification with custom notification', done => { 64 | let android = new GCMNotification(CONFIG); 65 | 66 | sinon.stub(android.getProvider(), 'send', (message, device, cb) => cb(null, 'RESULT')); 67 | 68 | android 69 | .send(['b2', 'c3'], { 70 | body: 'OVERRIDE_BODY' 71 | }) 72 | .then(result => { 73 | assert.equal(result, 'RESULT'); 74 | assert(android.getProvider().send.calledOnce); 75 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.body', 'OVERRIDE_BODY'); 76 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.title', 'TITLE'); 77 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.data.foo', 'bar'); 78 | assert.deepEqual(android.getProvider().send.getCall(0).args[1], ['a1', 'b2', 'c3']); 79 | assert.isFunction(android.getProvider().send.getCall(0).args[2]); 80 | 81 | android.getProvider().send.restore(); 82 | 83 | done(); 84 | }) 85 | .catch(done); 86 | }); 87 | 88 | it('Should properly send notification with extended notification', done => { 89 | let android = new GCMNotification(CONFIG); 90 | 91 | sinon.stub(android.getProvider(), 'send', (message, device, cb) => cb(null, 'RESULT')); 92 | 93 | android 94 | .send(['b2'], { 95 | body: 'OVERRIDE_BODY' 96 | }, { 97 | dryRun: true 98 | }) 99 | .then(result => { 100 | assert.equal(result, 'RESULT'); 101 | assert.ok(android.getProvider().send.calledOnce); 102 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.body', 'OVERRIDE_BODY'); 103 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.title', 'TITLE'); 104 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.dryRun', true); 105 | assert.deepEqual(android.getProvider().send.getCall(0).args[1], ['a1', 'b2']); 106 | assert.isFunction(android.getProvider().send.getCall(0).args[2]); 107 | 108 | android.getProvider().send.restore(); 109 | 110 | done(); 111 | }) 112 | .catch(done); 113 | }); 114 | 115 | it('Should properly send notification with all empty config', done => { 116 | let android = new GCMNotification(); 117 | 118 | sinon.stub(android.getProvider(), 'send', (message, device, cb) => cb(null, 'RESULT')); 119 | 120 | android 121 | .send() 122 | .then(result => { 123 | assert.equal(result, 'RESULT'); 124 | assert.ok(android.getProvider().send.calledOnce); 125 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.body', undefined); 126 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.title', undefined); 127 | assert.deepEqual(android.getProvider().send.getCall(0).args[1], []); 128 | assert.isFunction(android.getProvider().send.getCall(0).args[2]); 129 | 130 | android.getProvider().send.restore(); 131 | 132 | done(); 133 | }) 134 | .catch(done); 135 | }); 136 | 137 | it('Should properly send notification with empty pre-defined config and empty notification', done => { 138 | let android = new GCMNotification(); 139 | 140 | sinon.stub(android.getProvider(), 'send', (message, device, cb) => cb(null, 'RESULT')); 141 | 142 | android 143 | .send(['a1']) 144 | .then(result => { 145 | assert.equal(result, 'RESULT'); 146 | assert.ok(android.getProvider().send.calledOnce); 147 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.body', undefined); 148 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.title', undefined); 149 | assert.deepEqual(android.getProvider().send.getCall(0).args[1], ['a1']); 150 | assert.isFunction(android.getProvider().send.getCall(0).args[2]); 151 | 152 | android.getProvider().send.restore(); 153 | 154 | done(); 155 | }) 156 | .catch(done); 157 | }); 158 | 159 | it('Should properly send notification with empty pre-defined config and custom notification', done => { 160 | let android = new GCMNotification(); 161 | 162 | sinon.stub(android.getProvider(), 'send', (message, device, cb) => cb(null, 'RESULT')); 163 | 164 | android 165 | .send(['a1'], { 166 | title: 'CUSTOM_TITLE', 167 | body: 'CUSTOM_BODY' 168 | }, { 169 | dryRun: true 170 | }) 171 | .then(result => { 172 | assert.equal(result, 'RESULT'); 173 | assert.ok(android.getProvider().send.calledOnce); 174 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.title', 'CUSTOM_TITLE'); 175 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.body', 'CUSTOM_BODY'); 176 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.dryRun', true); 177 | assert.deepEqual(android.getProvider().send.getCall(0).args[1], ['a1']); 178 | assert.isFunction(android.getProvider().send.getCall(0).args[2]); 179 | 180 | android.getProvider().send.restore(); 181 | 182 | done(); 183 | }) 184 | .catch(done); 185 | }); 186 | 187 | it('Should properly reject promise on send notification', done => { 188 | let android = new GCMNotification(); 189 | 190 | sinon.stub(android.getProvider(), 'send', (message, device, cb) => cb(new Error('Some error occurred'))); 191 | 192 | android 193 | .send(['a1'], { 194 | title: 'CUSTOM_TITLE', 195 | body: 'CUSTOM_BODY' 196 | }, { 197 | dryRun: true 198 | }) 199 | .then(done) 200 | .catch(error => { 201 | assert.instanceOf(error, Error); 202 | assert.ok(android.getProvider().send.calledOnce); 203 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.title', 'CUSTOM_TITLE'); 204 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.notification.body', 'CUSTOM_BODY'); 205 | assert.deepPropertyVal(android.getProvider().send.getCall(0).args[0], 'params.dryRun', true); 206 | assert.deepEqual(android.getProvider().send.getCall(0).args[1], ['a1']); 207 | assert.isFunction(android.getProvider().send.getCall(0).args[2]); 208 | 209 | android.getProvider().send.restore(); 210 | 211 | done(); 212 | }); 213 | }); 214 | }); 215 | --------------------------------------------------------------------------------