├── .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 [](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 | }
--------------------------------------------------------------------------------