Skip to content

Commit 04140e1

Browse files
committed
FCM support
Credit goes to: https://github.com/jimnor0xF PR & Discussion: parse-community#222
1 parent 5536102 commit 04140e1

File tree

3 files changed

+226
-3
lines changed

3 files changed

+226
-3
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"dependencies": {
4343
"@parse/node-apn": "6.0.1",
4444
"@parse/node-gcm": "1.0.2",
45+
"firebase-admin": "11.10.1",
4546
"npmlog": "7.0.1",
4647
"parse": "4.2.0"
4748
},

src/FCM.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
"use strict";
2+
3+
import Parse from 'parse';
4+
import log from 'npmlog';
5+
import { initializeApp, cert, getApps, getApp } from 'firebase-admin/app';
6+
import { getMessaging } from 'firebase-admin/messaging';
7+
import { randomString } from './PushAdapterUtils';
8+
9+
const LOG_PREFIX = 'parse-server-push-adapter FCM';
10+
const FCMRegistrationTokensMax = 500;
11+
const FCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // FCM allows a max of 4 weeks
12+
13+
export default function FCM(args) {
14+
if (typeof args !== 'object' || !args.firebaseServiceAccount) {
15+
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
16+
'FCM Configuration is invalid');
17+
}
18+
19+
let app;
20+
if (getApps().length === 0) {
21+
app = initializeApp({credential: cert(args.firebaseServiceAccount)});
22+
}
23+
else {
24+
app = getApp();
25+
}
26+
this.sender = getMessaging(app);
27+
}
28+
29+
FCM.FCMRegistrationTokensMax = FCMRegistrationTokensMax;
30+
31+
/**
32+
* Send fcm request.
33+
* @param {Object} data The data we need to send, the format is the same with api request body
34+
* @param {Array} devices A array of devices
35+
* @returns {Object} Array of resolved promises
36+
*/
37+
38+
FCM.prototype.send = function(data, devices) {
39+
if (!data || !devices || !Array.isArray(devices)) {
40+
log.warn(LOG_PREFIX, 'invalid push payload');
41+
return;
42+
}
43+
44+
// We can only have 500 recepients per send, so we need to slice devices to
45+
// chunk if necessary
46+
const slices = sliceDevices(devices, FCM.FCMRegistrationTokensMax);
47+
48+
const sendToDeviceSlice = (deviceSlice) => {
49+
const pushId = randomString(10);
50+
const timestamp = Date.now();
51+
52+
// Build a device map
53+
const devicesMap = deviceSlice.reduce((memo, device) => {
54+
memo[device.deviceToken] = device;
55+
return memo;
56+
}, {});
57+
58+
const deviceTokens = Object.keys(devicesMap);
59+
const fcmPayload = generateFCMPayload(data, pushId, timestamp, deviceTokens);
60+
const length = deviceTokens.length;
61+
log.info(LOG_PREFIX, `sending push to ${length} devices`);
62+
63+
return this.sender.sendEachForMulticast(fcmPayload.data)
64+
.then((response) => {
65+
const promises = [];
66+
const failedTokens = [];
67+
const successfulTokens = [];
68+
69+
response.responses.forEach((resp, idx) => {
70+
if (resp.success) {
71+
successfulTokens.push(deviceTokens[idx]);
72+
promises.push(createSuccessfulPromise(deviceTokens[idx], devicesMap[deviceTokens[idx]].deviceType));
73+
} else {
74+
failedTokens.push(deviceTokens[idx]);
75+
promises.push(createErrorPromise(deviceTokens[idx], devicesMap[deviceTokens[idx]].deviceType, resp.error));
76+
log.error(LOG_PREFIX, `failed to send to ${deviceTokens[idx]} with error: ${JSON.stringify(resp.error)}`);
77+
}
78+
});
79+
80+
if (failedTokens.length) {
81+
log.error(LOG_PREFIX, `tokens with failed pushes: ${JSON.stringify(failedTokens)}`);
82+
}
83+
84+
if (successfulTokens.length) {
85+
log.verbose(LOG_PREFIX, `tokens with successful pushes: ${JSON.stringify(successfulTokens)}`);
86+
}
87+
88+
return Promise.all(promises);
89+
});
90+
};
91+
92+
const allPromises = Promise.all(slices.map(sendToDeviceSlice))
93+
.catch((err) => {
94+
log.error(LOG_PREFIX, `error sending push: ${err}`);
95+
});
96+
97+
return allPromises;
98+
}
99+
100+
/**
101+
* Generate the fcm payload from the data we get from api request.
102+
* @param {Object} requestData The request body
103+
* @param {String} pushId A random string
104+
* @param {Number} timeStamp A number in milliseconds since the Unix Epoch
105+
* @returns {Object} A payload for FCM
106+
*/
107+
function generateFCMPayload(requestData, pushId, timeStamp, deviceTokens) {
108+
delete requestData['where'];
109+
110+
const payloadToUse = {
111+
data: {},
112+
push_id: pushId,
113+
time: new Date(timeStamp).toISOString()
114+
};
115+
116+
// Use rawPayload instead of the GCM implementation if it exists
117+
if (requestData.hasOwnProperty('rawPayload')) {
118+
payloadToUse.data = {
119+
...requestData.rawPayload,
120+
tokens: deviceTokens
121+
};
122+
} else {
123+
// Android payload according to GCM implementation
124+
const androidPayload = {
125+
android: {
126+
priority: 'high'
127+
},
128+
tokens: deviceTokens
129+
};
130+
131+
if (requestData.hasOwnProperty('notification')) {
132+
androidPayload.notification = requestData.notification;
133+
}
134+
135+
if (requestData.hasOwnProperty('data')) {
136+
androidPayload.data = requestData.data;
137+
}
138+
139+
if (requestData['expiration_time']) {
140+
const expirationTime = requestData['expiration_time'];
141+
// Convert to seconds
142+
let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
143+
if (timeToLive < 0) {
144+
timeToLive = 0;
145+
}
146+
if (timeToLive >= FCMTimeToLiveMax) {
147+
timeToLive = FCMTimeToLiveMax;
148+
}
149+
150+
androidPayload.android.ttl = timeToLive;
151+
}
152+
153+
payloadToUse.data = androidPayload;
154+
}
155+
156+
return payloadToUse;
157+
}
158+
159+
/**
160+
* Slice a list of devices to several list of devices with fixed chunk size.
161+
* @param {Array} devices An array of devices
162+
* @param {Number} chunkSize The size of the a chunk
163+
* @returns {Array} An array which contains several arrays of devices with fixed chunk size
164+
*/
165+
function sliceDevices(devices, chunkSize) {
166+
const chunkDevices = [];
167+
while (devices.length > 0) {
168+
chunkDevices.push(devices.splice(0, chunkSize));
169+
}
170+
return chunkDevices;
171+
}
172+
173+
/**
174+
* Creates an errorPromise for return.
175+
*
176+
* @param {String} token Device-Token
177+
* @param {String} deviceType Device-Type
178+
* @param {String} errorMessage ErrrorMessage as string
179+
*/
180+
function createErrorPromise(token, deviceType, errorMessage) {
181+
return Promise.resolve({
182+
transmitted: false,
183+
device: {
184+
deviceToken: token,
185+
deviceType: deviceType
186+
},
187+
response: { error: errorMessage }
188+
});
189+
}
190+
191+
/**
192+
* Creates an successfulPromise for return.
193+
*
194+
* @param {String} token Device-Token
195+
* @param {String} deviceType Device-Type
196+
*/
197+
function createSuccessfulPromise(token, deviceType) {
198+
return Promise.resolve({
199+
transmitted: true,
200+
device: {
201+
deviceToken: token,
202+
deviceType: deviceType
203+
}
204+
});
205+
}
206+
207+
208+
FCM.generateFCMPayload = generateFCMPayload;
209+
210+
/* istanbul ignore else */
211+
if (process.env.TESTING) {
212+
FCM.sliceDevices = sliceDevices;
213+
}

src/ParsePushAdapter.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Parse from 'parse';
33
import log from 'npmlog';
44
import APNS from './APNS';
55
import GCM from './GCM';
6+
import FCM from './FCM';
67
import { classifyInstallations } from './PushAdapterUtils';
78

89
const LOG_PREFIX = 'parse-server-push-adapter';
@@ -30,11 +31,19 @@ export default class ParsePushAdapter {
3031
case 'ios':
3132
case 'tvos':
3233
case 'osx':
33-
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
34+
if (pushConfig[pushType].hasOwnProperty('firebaseServiceAccount')) {
35+
this.senderMap[pushType] = new FCM(pushConfig[pushType]);
36+
} else {
37+
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
38+
}
3439
break;
3540
case 'android':
3641
case 'fcm':
37-
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
42+
if (pushConfig[pushType].hasOwnProperty('firebaseServiceAccount')) {
43+
this.senderMap[pushType] = new FCM(pushConfig[pushType]);
44+
} else {
45+
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
46+
}
3847
break;
3948
}
4049
}
@@ -76,4 +85,4 @@ export default class ParsePushAdapter {
7685
return [].concat.apply([], promises);
7786
})
7887
}
79-
}
88+
}

0 commit comments

Comments
 (0)