├── .github └── workflows │ └── npmpublish.yml ├── .gitignore ├── .snyk ├── LICENSE ├── README.md ├── example.js ├── index.js ├── lib ├── fcm.js ├── topic_data.js ├── topic_options.js └── topic_request.js ├── package-lock.json ├── package.json └── test ├── fcm_topic_request.test.js ├── mocha.opts ├── topic_data.test.js ├── topic_options.test.js └── topic_request.test.js /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | 35 | publish-gpr: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: 12 43 | registry-url: https://npm.pkg.github.com/ 44 | scope: '@your-github-username' 45 | - run: npm ci 46 | - run: npm publish 47 | env: 48 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | test.js 36 | .idea/ 37 | -------------------------------------------------------------------------------- /.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-HTTPSPROXYAGENT-469131: 7 | - firebase-admin > @google-cloud/storage > teeny-request > https-proxy-agent: 8 | patched: '2020-03-09T17:42:08.007Z' 9 | - firebase-admin > @google-cloud/storage > gcs-resumable-upload > gaxios > https-proxy-agent: 10 | patched: '2020-03-09T17:42:08.007Z' 11 | - firebase-admin > @google-cloud/firestore > google-gax > google-auth-library > https-proxy-agent: 12 | patched: '2020-03-09T17:42:08.007Z' 13 | - firebase-admin > @google-cloud/storage > @google-cloud/common > google-auth-library > https-proxy-agent: 14 | patched: '2020-03-09T17:42:08.007Z' 15 | - firebase-admin > @google-cloud/storage > gcs-resumable-upload > google-auth-library > https-proxy-agent: 16 | patched: '2020-03-09T17:42:08.007Z' 17 | - firebase-admin > @google-cloud/firestore > google-gax > google-auth-library > gaxios > https-proxy-agent: 18 | patched: '2020-03-09T17:42:08.007Z' 19 | - firebase-admin > @google-cloud/storage > @google-cloud/common > google-auth-library > gaxios > https-proxy-agent: 20 | patched: '2020-03-09T17:42:08.007Z' 21 | - firebase-admin > @google-cloud/storage > gcs-resumable-upload > google-auth-library > gaxios > https-proxy-agent: 22 | patched: '2020-03-09T17:42:08.007Z' 23 | - firebase-admin > @google-cloud/firestore > google-gax > google-auth-library > gcp-metadata > gaxios > https-proxy-agent: 24 | patched: '2020-03-09T17:42:08.007Z' 25 | - firebase-admin > @google-cloud/storage > @google-cloud/common > google-auth-library > gcp-metadata > gaxios > https-proxy-agent: 26 | patched: '2020-03-09T17:42:08.007Z' 27 | - firebase-admin > @google-cloud/storage > gcs-resumable-upload > google-auth-library > gcp-metadata > gaxios > https-proxy-agent: 28 | patched: '2020-03-09T17:42:08.007Z' 29 | - firebase-admin > @google-cloud/firestore > google-gax > google-auth-library > gtoken > gaxios > https-proxy-agent: 30 | patched: '2020-03-09T17:42:08.007Z' 31 | - firebase-admin > @google-cloud/storage > @google-cloud/common > google-auth-library > gtoken > gaxios > https-proxy-agent: 32 | patched: '2020-03-09T17:42:08.007Z' 33 | - firebase-admin > @google-cloud/storage > gcs-resumable-upload > google-auth-library > gtoken > gaxios > https-proxy-agent: 34 | patched: '2020-03-09T17:42:08.007Z' 35 | SNYK-JS-LODASH-450202: 36 | - firebase-admin > @google-cloud/storage > async > lodash: 37 | patched: '2020-03-09T17:42:08.007Z' 38 | SNYK-JS-LODASH-567746: 39 | - snyk > lodash: 40 | patched: '2020-05-01T05:05:32.495Z' 41 | - snyk > @snyk/dep-graph > lodash: 42 | patched: '2020-05-01T05:05:32.495Z' 43 | - snyk > inquirer > lodash: 44 | patched: '2020-05-01T05:05:32.495Z' 45 | - snyk > snyk-config > lodash: 46 | patched: '2020-05-01T05:05:32.495Z' 47 | - snyk > snyk-mvn-plugin > lodash: 48 | patched: '2020-05-01T05:05:32.495Z' 49 | - snyk > snyk-nodejs-lockfile-parser > lodash: 50 | patched: '2020-05-01T05:05:32.495Z' 51 | - snyk > snyk-nuget-plugin > lodash: 52 | patched: '2020-05-01T05:05:32.495Z' 53 | - snyk > @snyk/dep-graph > graphlib > lodash: 54 | patched: '2020-05-01T05:05:32.495Z' 55 | - snyk > snyk-go-plugin > graphlib > lodash: 56 | patched: '2020-05-01T05:05:32.495Z' 57 | - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: 58 | patched: '2020-05-01T05:05:32.495Z' 59 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > lodash: 60 | patched: '2020-05-01T05:05:32.495Z' 61 | - snyk > snyk-nuget-plugin > dotnet-deps-parser > lodash: 62 | patched: '2020-05-01T05:05:32.495Z' 63 | - snyk > snyk-php-plugin > @snyk/composer-lockfile-parser > lodash: 64 | patched: '2020-05-01T05:05:32.495Z' 65 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash: 66 | patched: '2020-05-01T05:05:32.495Z' 67 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/ruby-semver > lodash: 68 | patched: '2020-05-01T05:05:32.495Z' 69 | - snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash: 70 | patched: '2020-05-01T05:05:32.495Z' 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Saber Tecnologias Educacionais e Sociais 4 | Copyright (c) 2016 João Leonardo Pereira 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Warning: on February 2, 2017, the Firebase Team [released][11] the [_admin.messaging()_][12] service to their node.js admin module. This new service makes this module kind of *deprecated* 2 | 3 | fcm-node [![NPM version](https://badge.fury.io/js/fcm-node.svg)](http://badge.fury.io/js/fcm-node) 4 | ======== 5 | A Node.JS simple interface to Google's Firebase Cloud Messaging (FCM). Supports both android and iOS, including topic messages, and parallel calls. 6 | Aditionally it also keeps the callback behavior for the new firebase messaging service. 7 | ## Installation 8 | 9 | Via [npm][1]: 10 | 11 | $ npm install fcm-node 12 | 13 | ## Usage 14 | 15 | There are 2 ways to use this lib: 16 | ### The **classic** one 17 | 1. Generate a **Server Key** on your app's firebase console and pass it to the **FCM** constructor 18 | 2. Create a _message object_ and call the **send()** function 19 | #### Classic usage example: 20 | ```js 21 | var FCM = require('fcm-node'); 22 | var serverKey = 'YOURSERVERKEYHERE'; //put your server key here 23 | var fcm = new FCM(serverKey); 24 | 25 | var message = { //this may vary according to the message type (single recipient, multicast, topic, et cetera) 26 | to: 'registration_token', 27 | collapse_key: 'your_collapse_key', 28 | 29 | notification: { 30 | title: 'Title of your push notification', 31 | body: 'Body of your push notification' 32 | }, 33 | 34 | data: { //you can send only notification or only data(or include both) 35 | my_key: 'my value', 36 | my_another_key: 'my another value' 37 | } 38 | }; 39 | 40 | fcm.send(message, function(err, response){ 41 | if (err) { 42 | console.log("Something has gone wrong!"); 43 | } else { 44 | console.log("Successfully sent with response: ", response); 45 | } 46 | }); 47 | ``` 48 | 49 | ### The **new** one 50 | 1. Go to your [Service account tab][13] in your project's settings and download/generate your app's private key. 51 | 2. Add this file in your project's workspace 52 | 3. Import that file with a `require('path/to/privatekey.json')` style call and pass the object to the **FCM** constructor 53 | 4. Create a _message object_ and call the **send()** function 54 | 55 | #### "New" usage example 56 | ```js 57 | const FCM = require('fcm-node') 58 | 59 | var serverKey = require('path/to/privatekey.json') //put the generated private key path here 60 | 61 | var fcm = new FCM(serverKey) 62 | 63 | var message = { //this may vary according to the message type (single recipient, multicast, topic, et cetera) 64 | to: 'registration_token', 65 | collapse_key: 'your_collapse_key', 66 | 67 | notification: { 68 | title: 'Title of your push notification', 69 | body: 'Body of your push notification' 70 | }, 71 | 72 | data: { //you can send only notification or only data(or include both) 73 | my_key: 'my value', 74 | my_another_key: 'my another value' 75 | } 76 | } 77 | 78 | fcm.send(message, function(err, response){ 79 | if (err) { 80 | console.log("Something has gone wrong!") 81 | } else { 82 | console.log("Successfully sent with response: ", response) 83 | } 84 | }) 85 | ``` 86 | #### Multi client support (thanks to @nswbmw) 87 | ``` 88 | const FCM = require('fcm-node') 89 | 90 | let fcm1 = new FCM(KEY_1) 91 | let fcm2 = new FCM(KEY_2) 92 | ``` 93 | 94 | ## Topic subscription on web clients 95 | 96 | Web clients doesn't have a "native" way to subscribe/unsubscribe from topics other than manually requesting, managing and registering with the google's iid servers. To resolve this "barrier" your server can easily handle the web client's sub/unsub requests with this lib. 97 | 98 | For more detailed information, please take a look at [Google InstanceID Reference][14]. 99 | 100 | *PS: For mobile clients you can still use the native calls to subscribe/unsubscribe with one-liner calls* 101 | ##### Android 102 | ```java 103 | FirebaseMessaging.getInstance().subscribeToTopic("news"); 104 | ``` 105 | ##### iOS 106 | ```objective-c 107 | [[FIRMessaging messaging] subscribeToTopic:@"/topics/news"]; 108 | ``` 109 | 110 | 111 | 112 | ### Subscribe Device Tokens to Topics 113 | 114 | ```js 115 | var FCM = require('fcm-node'); 116 | var serverKey = 'YOURSERVERKEYHERE'; //put your server key here 117 | var fcm = new FCM(serverKey); 118 | 119 | fcm.subscribeToTopic([ 'device_token_1', 'device_token_2' ], 'some_topic_name', (err, res) => { 120 | assert.ifError(err); 121 | assert.ok(res); 122 | done(); 123 | }); 124 | ``` 125 | 126 | ### Unsubscribe Device Tokens to Topics 127 | 128 | ```js 129 | var FCM = require('fcm-node'); 130 | var serverKey = 'YOURSERVERKEYHERE'; //put your server key here 131 | var fcm = new FCM(serverKey); 132 | 133 | fcm.unsubscribeToTopic([ 'device_token_1', 'device_token_2' ], 'some_topic_name', (err, res) => { 134 | assert.ifError(err); 135 | assert.ok(res); 136 | done(); 137 | }); 138 | 139 | ``` 140 | 141 | ## Notes 142 | * See [FCM documentation][2] for general details. 143 | * See [Firebase Cloud Messaging HTTP Protocol][10] for details about the HTTP syntax used and JSON fields, notification and data objects. **(STRONGLY RECOMMENDED)** 144 | * On **iOS**, set **content_available** to **true** to receive data while your app is in background. (As seen in [FCM Docs][8]) 145 | 146 | ## Credits 147 | 148 | Extended by [Leonardo Pereira (me)][3]. 149 | Based on the great work on [fcm-push][7] by [Rasmunandar Rustam][4] cloned and modified from there, which in its turn, was cloned and modified from [Changshin Lee][5]'s [node-gcm][5] 150 | 151 | ## License 152 | 153 | [MIT][6] 154 | 155 | [1]: http://github.com/isaacs/npm 156 | [2]: https://firebase.google.com/docs/cloud-messaging/server 157 | [3]: https://github.com/jlcvp 158 | [4]: mailto:nandar.rustam@gmail.com 159 | [5]: https://github.com/h2soft/node-gcm 160 | [6]: https://opensource.org/licenses/MIT 161 | [7]: https://github.com/nandarustam/fcm-push 162 | [8]: https://firebase.google.com/docs/cloud-messaging/concept-options 163 | [9]: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH101-SW2 164 | [10]: https://firebase.google.com/docs/cloud-messaging/http-server-ref 165 | [11]: https://firebase.google.com/support/release-notes/admin/node 166 | [12]: https://firebase.google.com/docs/reference/admin/node/admin.messaging 167 | [13]: https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk 168 | [14]: https://developers.google.com/instance-id/reference/server#create_relationship_maps_for_app_instances 169 | [15]: https://github.com/sofiapm 170 | [16]: https://github.com/crackjack 171 | [17]: https://github.com/cesardmoro 172 | [18]: https://github.com/nswbmw 173 | 174 | ## Changelog 175 | 1.6.0 - Multi client support - *Thanks to [@nswbmw][18] for this feature* 176 | 1.5.2 - fixed a bug where the send callback was being called twice - *Thanks to [@cesardmoro][17] for this fix* 177 | 1.3.0 - Added proxy capabilities - *Thanks to [@crackjack][16] for this feature* 178 | 1.2.0 - Added topic subscriptions management for web clients - *Thanks to [@sofiapm][15] for this feature* 179 | 1.1.0 - Support for the new firebase node.js sdk methods 180 | 1.0.14 - Added example file to quick tests
181 | 1.0.13 - Added a error response in case of TopicsMessageRateExceeded response
182 | 1.0.12 - Refactored the client removing the Event Emitter's Logic to fix concurrency issues. Using pure callbacks now also avoids memory leak in specific scenarios with lots of parallel calls to send function.
183 | 1.0.11 - \ send function returning error objects when multicast messages (or individually targeted) returned both error and success keys on response message (even with error counter = 0 )
184 | 1.0.9 - Updated Documentation
185 | 1.0.8 - \ 'icon' field no longer required in notification
186 | 1.0.7 - renaming repository
187 | 1.0.6 - bugfix: send function was always returning an error object for multicast messages (multiple registration ids)
188 | 1.0.5 - bugfix with UTF-8 enconding and chunk-encoded transfers
189 | 1.0.1 - forked from fcm-push and extended to accept topic messages without errors
190 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Leonardo on 02/08/2016. 3 | */ 4 | FCM = require('fcm-node'); 5 | 6 | 7 | var SERVER_API_KEY='your_api_key';//put your api key here 8 | 9 | var validDeviceRegistrationToken = 'c1m7I:A ... bjj4SK-'; //put a valid device token here 10 | 11 | 12 | 13 | 14 | var fcmCli= new FCM(SERVER_API_KEY); 15 | 16 | var payloadOK = { 17 | to: validDeviceRegistrationToken, 18 | data: { //some data object (optional) 19 | url: 'news', 20 | foo:'fooooooooooooo', 21 | bar:'bar bar bar' 22 | }, 23 | priority: 'high', 24 | content_available: true, 25 | notification: { //notification object 26 | title: 'HELLO', body: 'World!', sound : "default", badge: "1" 27 | } 28 | }; 29 | 30 | var payloadError = { 31 | to: "4564654654654654", //invalid registration token 32 | data: { 33 | url: "news" 34 | }, 35 | priority: 'high', 36 | content_available: true, 37 | notification: { title: 'TEST HELLO', body: '123', sound : "default", badge: "1" } 38 | }; 39 | 40 | var payloadMulticast = { 41 | registration_ids:["4564654654654654", 42 | '123123123', 43 | validDeviceRegistrationToken, //valid token among invalid tokens to see the error and ok response 44 | '123133213123123'], 45 | data: { 46 | url: "news" 47 | }, 48 | priority: 'high', 49 | content_available: true, 50 | notification: { title: 'Hello', body: 'Multicast', sound : "default", badge: "1" } 51 | }; 52 | 53 | var callbackLog = function (sender, err, res) { 54 | console.log("\n__________________________________") 55 | console.log("\t"+sender); 56 | console.log("----------------------------------") 57 | console.log("err="+err); 58 | console.log("res="+res); 59 | console.log("----------------------------------\n>>>"); 60 | }; 61 | 62 | function sendOK() 63 | { 64 | fcmCli.send(payloadOK,function(err,res){ 65 | callbackLog('sendOK',err,res); 66 | }); 67 | } 68 | 69 | function sendError() { 70 | fcmCli.send(payloadError,function(err,res){ 71 | callbackLog('sendError',err,res); 72 | }); 73 | } 74 | 75 | function sendMulticast(){ 76 | fcmCli.send(payloadMulticast,function(err,res){ 77 | callbackLog('sendMulticast',err,res); 78 | }); 79 | } 80 | 81 | 82 | sendOK(); 83 | sendMulticast(); 84 | sendError(); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/fcm'); -------------------------------------------------------------------------------- /lib/fcm.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | var HttpsProxyAgent = require('https-proxy-agent'); 3 | var retry = require('retry'); 4 | var firebaseadmin = require("firebase-admin"); 5 | const TopicRequest = require('../lib/topic_request'); 6 | const TopicOptions = require('../lib/topic_options'); 7 | const TopicData = require('../lib/topic_data'); 8 | 9 | 10 | function FCM(accountKey, proxy_url=null, name=null) { 11 | var admin = null 12 | if(!proxy_url) { 13 | proxy_url = process.env.http_proxy || null; 14 | } 15 | if(!accountKey) { 16 | throw Error('You must provide the APIKEY for your firebase application.'); 17 | } 18 | else if(typeof accountKey == 'string') { //API KEY PASSED string, legacy use 19 | 20 | this.serverKey = accountKey; 21 | 22 | this.fcmOptions = { 23 | host: 'fcm.googleapis.com', 24 | port: 443, 25 | path: '/fcm/send', 26 | method: 'POST', 27 | headers: {} 28 | }; 29 | 30 | this.send = function (payload, CB) { 31 | 32 | var self = this; 33 | if (!CB) { 34 | throw Error('you must provide a callback function(err,result)'); //just in case 35 | } 36 | else { 37 | var operation = retry.operation(); 38 | var mpayload = JSON.stringify(payload); 39 | var mFcmOptions = Object.assign({}, self.fcmOptions); //copying the fcmOptions object to avoid problems in parallel calls 40 | 41 | if(proxy_url) { 42 | // HTTP/HTTPS proxy to connect to 43 | var proxy = proxy_url; 44 | var agent = new HttpsProxyAgent(proxy); 45 | 46 | mFcmOptions.agent = agent; 47 | } 48 | 49 | operation.attempt(function (currentAttempt) { 50 | var headers = { 51 | 'Host': mFcmOptions.host, 52 | 'Authorization': 'key=' + self.serverKey, 53 | 'Content-Type': 'application/json' 54 | //'Content-Length': mpayload.length //removed this line for chunk-encoded transfer compatibility (UTF-8 and all non-ANSI codification) 55 | }; 56 | 57 | mFcmOptions.headers = headers; 58 | 59 | if (self.keepAlive) headers.Connection = 'keep-alive'; 60 | 61 | var request = https.request(mFcmOptions, function (res) { 62 | var data = ''; 63 | 64 | 65 | if (res.statusCode == 503) { 66 | // If the server is temporary unavailable, the FCM spec requires that we implement exponential backoff 67 | // and respect any Retry-After header 68 | if (res.headers['retry-after']) { 69 | var retrySeconds = res.headers['retry-after'] * 1; // force number 70 | if (isNaN(retrySeconds)) { 71 | // The Retry-After header is a HTTP-date, try to parse it 72 | retrySeconds = new Date(res.headers['retry-after']).getTime() - new Date().getTime(); 73 | } 74 | if (!isNaN(retrySeconds) && retrySeconds > 0) { 75 | operation._timeouts['minTimeout'] = retrySeconds; 76 | } 77 | } 78 | if (!operation.retry('TemporaryUnavailable')) { 79 | CB(operation.mainError(), null); 80 | } 81 | // Ignore all subsequent events for this request 82 | return; 83 | } 84 | 85 | function respond() { 86 | var error = null, id = null; 87 | 88 | //Handle the various responses 89 | if (data.indexOf('\"multicast_id\":') > -1)//multicast_id success 90 | { 91 | var anyFail = ((JSON.parse(data)).failure > 0); 92 | 93 | if (anyFail) { 94 | error = data.substring(0).trim(); 95 | } 96 | 97 | var anySuccess = ((JSON.parse(data)).success > 0); 98 | 99 | if (anySuccess) { 100 | id = data.substring(0).trim(); 101 | } 102 | } else if (data.indexOf('\"message_id\":') > -1) { //topic messages success 103 | id = data; 104 | } else if (data.indexOf('\"error\":') > -1) { //topic messages error 105 | error = data; 106 | } else if (data.indexOf('TopicsMessageRateExceeded') > -1) { 107 | error = 'TopicsMessageRateExceededError' 108 | } else if (data.indexOf('Unauthorized') > -1) { 109 | error = 'NotAuthorizedError' 110 | } else { 111 | error = 'InvalidServerResponse'; 112 | } 113 | // Only retry if error is QuotaExceeded or DeviceQuotaExceeded 114 | if (operation.retry(currentAttempt <= 3 && ['QuotaExceeded', 'DeviceQuotaExceeded', 'InvalidServerResponse'].indexOf(error) >= 0 ? error : null)) { 115 | return; 116 | } 117 | // Success, return message id (without id=) 118 | CB(error, id); 119 | } 120 | 121 | res.on('data', function (chunk) { 122 | data += chunk; 123 | }); 124 | res.on('end', respond); 125 | }); 126 | 127 | request.on('error', function (error) { 128 | CB(error, null); 129 | }); 130 | 131 | request.end(mpayload); 132 | }); 133 | } 134 | } 135 | 136 | // Subscribe devices to topic 137 | // If topic does not exist, a new one is created 138 | this.subscribeToTopic = (deviceTokens, topicName, CB) => { 139 | 140 | const options = TopicOptions('iid.googleapis.com', '/iid/v1:batchAdd', 'POST', this.serverKey.slice(0)); 141 | const subscriptionData = TopicData(topicName, deviceTokens); 142 | 143 | TopicRequest(options, subscriptionData, (err, res) => { 144 | CB(err, res); 145 | }); 146 | } 147 | 148 | // Unsubscribe device to topic 149 | this.unsubscribeToTopic = (deviceTokens, topicName, CB) => { 150 | const options = TopicOptions('iid.googleapis.com', '/iid/v1:batchRemove', 'POST', this.serverKey.slice(0)); 151 | const unsubscriptionData = TopicData(topicName, deviceTokens); 152 | 153 | TopicRequest(options, unsubscriptionData, (err, res) => { 154 | CB(err, res); 155 | }); 156 | } 157 | } 158 | else{ //accountkey object passed, new SDK 'de-promisefy' use 159 | const config = 'private_key' in accountKey || 'privateKey' in accountKey ? 160 | {credential: firebaseadmin.credential.cert(accountKey)} : 161 | accountKey; 162 | 163 | if(name) { 164 | admin = firebaseadmin.initializeApp(config, name); 165 | } else { 166 | admin = firebaseadmin.initializeApp(config); 167 | } 168 | this.send = function(payload, _callback){ 169 | if (!_callback) { 170 | throw Error('You must provide a callback function(err,result)') 171 | } 172 | else{ 173 | if(!payload) _callback(new Error('You must provide a payload object')) 174 | else{ 175 | if(payload.to) { 176 | if (typeof payload.to == 'string') { 177 | var to = payload.to 178 | delete payload.to 179 | if (to.startsWith('/topics/')) { 180 | var topic = to.slice(8)//anything after '/topics/' 181 | 182 | admin.messaging().sendToTopic(topic, payload) 183 | .then(function(response){_callback(null, response)}) 184 | .catch(function (err) {_callback(err)}) 185 | } 186 | else{ 187 | admin.messaging().sendToDevice(to,payload) 188 | .then(function (response) {_callback(null,response)}) 189 | .catch(function (error) {_callback(error)}) 190 | } 191 | } 192 | else{ 193 | var err = new Error('Invalid "to" field in payload'); 194 | _callback(err) 195 | } 196 | } 197 | else if(payload.registration_ids){ 198 | var regIds = payload.registration_ids; 199 | delete payload.registration_ids; 200 | if(regIds instanceof Array && typeof regIds[0] == 'string') 201 | { 202 | admin.messaging().sendToDevice(regIds, payload) 203 | .then(function (response) {_callback(null,response)}) 204 | .catch(function (error) {_callback(error)}) 205 | } 206 | else{ 207 | var err = new Error('Invalid "registration_ids" field in payload'); 208 | _callback(err) 209 | } 210 | } 211 | else{ 212 | var err = new Error('Invalid payload object'); 213 | _callback(err) 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | 221 | module.exports = FCM; 222 | -------------------------------------------------------------------------------- /lib/topic_data.js: -------------------------------------------------------------------------------- 1 | function TopicData(topicName, registration_tokens) { 2 | return { 3 | to: `/topics/${topicName}`, 4 | registration_tokens 5 | }; 6 | } 7 | 8 | module.exports = TopicData; -------------------------------------------------------------------------------- /lib/topic_options.js: -------------------------------------------------------------------------------- 1 | function TopicOptions(host, path, method, serverKey) { 2 | this.topicOptions = { 3 | host, 4 | path, 5 | method, 6 | json: true, 7 | headers: { } 8 | }; 9 | 10 | this.topicOptions.headers = { 11 | 'Host': topicOptions.host, 12 | 'Authorization': 'key=' + serverKey, 13 | 'Content-Type': 'application/json', 14 | 'Accept': 'application/json' 15 | }; 16 | 17 | return topicOptions; 18 | } 19 | 20 | module.exports = TopicOptions; -------------------------------------------------------------------------------- /lib/topic_request.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | 3 | function TopicRequest(options, data, CB){ 4 | const payload = JSON.stringify(data); 5 | const request = https.request(options, (res) => { 6 | "use strict"; 7 | let body = ''; 8 | 9 | if(res.statusCode !== 200){ 10 | CB({ 11 | statusCode: res.statusCode, 12 | message: res.statusMessage 13 | }, null); 14 | }else{ 15 | res.on('data', function(chunk){ 16 | body += chunk; 17 | }); 18 | 19 | res.on('end', function(){ 20 | CB(null, JSON.parse(body)) 21 | }); 22 | } 23 | }).on('error', (e) => { 24 | CB(JSON.parse(e), null); 25 | }).end(payload); 26 | } 27 | 28 | module.exports = TopicRequest; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fcm-node", 3 | "version": "1.6.1", 4 | "description": "A Node.JS simple interface to Google's Firebase Cloud Messaging (FCM). Supports both android and iOS, including topic messages, and parallel calls.\nAditionally it also keeps the callback behavior for the new firebase messaging service.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/jlcvp/fcm-node.git" 9 | }, 10 | "keywords": [ 11 | "fcm", 12 | "gcm", 13 | "push", 14 | "notification", 15 | "push notification", 16 | "firebase", 17 | "firebase cloud messaging", 18 | "google", 19 | "android", 20 | "ios", 21 | "topic message", 22 | "parallel send" 23 | ], 24 | "author": "Leonardo Pereira ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/jlcvp/fcm-node/issues" 28 | }, 29 | "homepage": "https://github.com/jlcvp/fcm-node", 30 | "dependencies": { 31 | "firebase-admin": "^9.6.0", 32 | "https-proxy-agent": "^5.0.0", 33 | "retry": "^0.12.0", 34 | "snyk": "^1.518.0" 35 | }, 36 | "devDependencies": { 37 | "mocha": "^8.3.2" 38 | }, 39 | "scripts": { 40 | "test": "mocha --exit", 41 | "debugtest": "mocha --debug-brk $npm_package_options_mocha --exit", 42 | "snyk-protect": "snyk protect", 43 | "prepare": "npm run snyk-protect" 44 | }, 45 | "directories": { 46 | "test": "test" 47 | }, 48 | "tonicExampleFilename": "example.js", 49 | "snyk": true 50 | } 51 | -------------------------------------------------------------------------------- /test/fcm_topic_request.test.js: -------------------------------------------------------------------------------- 1 | const fcm = require('../index.js'); 2 | 3 | // must replace 'SERVER_KEY' for you key 4 | const FCM = new fcm('SERVER_KEY'); 5 | var assert = require('assert'); 6 | 7 | module.exports = { 8 | 9 | 'test FCM.subscribeToTopic() ': function (done) { 10 | FCM.subscribeToTopic([ 'test_topic' ], 'some_topic_name', (err, res) => { 11 | assert.ifError(err); 12 | assert.ok(res); 13 | done(); 14 | }); 15 | }, 16 | 'test FCM.unsubscribeToTopic() ': function (done) { 17 | FCM.unsubscribeToTopic([ 'test_topic' ], 'some_topic_name', (err, res) => { 18 | assert.ifError(err); 19 | assert.ok(res); 20 | done(); 21 | }); 22 | } 23 | } -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui exports 2 | --slow 1500ms 3 | --timeout 10000ms 4 | --reporter spec -------------------------------------------------------------------------------- /test/topic_data.test.js: -------------------------------------------------------------------------------- 1 | const TopicData = require('../lib/topic_data'); 2 | var assert = require('assert'); 3 | 4 | module.exports = { 5 | 6 | 'test TopicData() ': function () { 7 | const topicName = 'some-topic-name'; 8 | const deviceTokens = [ 'token_1', 'token_2' ]; 9 | 10 | const data = TopicData(topicName, deviceTokens); 11 | 12 | assert.equal(data.registration_tokens, deviceTokens); 13 | assert.equal(data.to, `/topics/${topicName}`); 14 | }, 15 | 16 | } -------------------------------------------------------------------------------- /test/topic_options.test.js: -------------------------------------------------------------------------------- 1 | const TopicOptions = require('../lib/topic_options.js'); 2 | var assert = require('assert'); 3 | 4 | module.exports = { 5 | 6 | 'test TopicOptions() ': function () { 7 | const host = 'iid.googleapis.com/iid'; 8 | const path = '/v1:batchAdd'; 9 | const method = 'POST'; 10 | const serverKey = 'SERVER_KEY'; 11 | 12 | const options = TopicOptions(host, path, method, serverKey); 13 | assert.equal(options.host, host); 14 | assert.equal(options.path, path); 15 | assert.equal(options.method, method); 16 | assert.equal(options.headers.Host, host); 17 | assert.equal(options.headers.Authorization, 'key=SERVER_KEY'); 18 | }, 19 | 20 | } -------------------------------------------------------------------------------- /test/topic_request.test.js: -------------------------------------------------------------------------------- 1 | const TopicRequest = require('../lib/topic_request'); 2 | const TopicOptions = require('../lib/topic_options'); 3 | const TopicData = require('../lib/topic_data'); 4 | var assert = require('assert'); 5 | 6 | module.exports = { 7 | 8 | 'test TopicRequest() ': function (done) { 9 | 10 | // must replace 'SERVER_KEY' for you key 11 | const options = TopicOptions('iid.googleapis.com', '/iid/v1:batchAdd', 'POST', 'SERVER_KEY'); 12 | const data = TopicData('some_topic_name', [ 'teste' ]); 13 | TopicRequest(options, data, (err, res) => { 14 | assert.ifError(err); 15 | assert.ok(res); 16 | done(); 17 | }); 18 | } 19 | } --------------------------------------------------------------------------------