forked from parse-community/parse-server-push-adapter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGCM.js
179 lines (165 loc) · 5.87 KB
/
GCM.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"use strict";
import Parse from 'parse';
import log from 'npmlog';
import gcm from '@parse/node-gcm';
import { randomString } from './PushAdapterUtils.js';
const LOG_PREFIX = 'parse-server-push-adapter GCM';
const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
const GCMRegistrationTokensMax = 1000;
export default function GCM(args) {
if (typeof args !== 'object' || !args.apiKey) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'GCM Configuration is invalid');
}
this.sender = new gcm.Sender(args.apiKey, args.requestOptions);
}
GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;
/**
* Send gcm request.
* @param {Object} data The data we need to send, the format is the same with api request body
* @param {Array} devices A array of devices
* @returns {Object} A promise which is resolved after we get results from gcm
*/
GCM.prototype.send = function(data, devices) {
if (!data || !devices || !Array.isArray(devices)) {
log.warn(LOG_PREFIX, 'invalid push payload');
return;
}
const pushId = randomString(10);
// Make a new array
devices = devices.slice(0);
const timestamp = Date.now();
// For android, we can only have 1000 recepients per send, so we need to slice devices to
// chunk if necessary
const slices = sliceDevices(devices, GCM.GCMRegistrationTokensMax);
if (slices.length > 1) {
log.verbose(LOG_PREFIX, `the number of devices exceeds ${GCMRegistrationTokensMax}`);
// Make 1 send per slice
const promises = slices.reduce((memo, slice) => {
const promise = this.send(data, slice, timestamp);
memo.push(promise);
return memo;
}, [])
return Promise.all(promises).then((results) => {
const allResults = results.reduce((memo, result) => {
return memo.concat(result);
}, []);
return Promise.resolve(allResults);
});
}
// get the devices back...
devices = slices[0];
let expirationTime;
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date
// in Unix epoch time in milliseconds here
if (data['expiration_time']) {
expirationTime = data['expiration_time'];
}
// Generate gcm payload
// PushId is not a formal field of GCM, but Parse Android SDK uses this field to deduplicate push notifications
const gcmPayload = generateGCMPayload(data, pushId, timestamp, expirationTime);
// Make and send gcm request
const message = new gcm.Message(gcmPayload);
// Build a device map
const devicesMap = devices.reduce((memo, device) => {
memo[device.deviceToken] = device;
return memo;
}, {});
const deviceTokens = Object.keys(devicesMap);
const resolvers = [];
const promises = deviceTokens.map(() => new Promise(resolve => resolvers.push(resolve)));
const registrationTokens = deviceTokens;
const length = registrationTokens.length;
log.verbose(LOG_PREFIX, `sending to ${length} ${length > 1 ? 'devices' : 'device'}`);
this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
// example response:
/*
{ "multicast_id":7680139367771848000,
"success":0,
"failure":4,
"canonical_ids":0,
"results":[ {"error":"InvalidRegistration"},
{"error":"InvalidRegistration"},
{"error":"InvalidRegistration"},
{"error":"InvalidRegistration"}] }
*/
if (error) {
log.error(LOG_PREFIX, `send errored: %s`, JSON.stringify(error, null, 4));
} else {
log.verbose(LOG_PREFIX, `GCM Response: %s`, JSON.stringify(response, null, 4));
}
const { results, multicast_id } = response || {};
registrationTokens.forEach((token, index) => {
const resolve = resolvers[index];
const result = results ? results[index] : undefined;
const device = devicesMap[token];
device.deviceType = 'android';
const resolution = {
device,
multicast_id,
response: error || result,
};
if (!result || result.error) {
resolution.transmitted = false;
} else {
resolution.transmitted = true;
}
resolve(resolution);
});
});
return Promise.all(promises);
}
/**
* Generate the gcm payload from the data we get from api request.
* @param {Object} requestData The request body
* @param {String} pushId A random string
* @param {Number} timeStamp A number whose format is the Unix Epoch
* @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined
* @returns {Object} A promise which is resolved after we get results from gcm
*/
function generateGCMPayload(requestData, pushId, timeStamp, expirationTime) {
const payload = {
priority: 'high'
};
payload.data = {
data: requestData.data,
push_id: pushId,
time: new Date(timeStamp).toISOString()
}
const optionalKeys = ['contentAvailable', 'notification'];
optionalKeys.forEach((key) => {
if (requestData.hasOwnProperty(key)) {
payload[key] = requestData[key];
}
});
if (expirationTime) {
// The timeStamp and expiration is in milliseconds but gcm requires second
let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
if (timeToLive < 0) {
timeToLive = 0;
}
if (timeToLive >= GCMTimeToLiveMax) {
timeToLive = GCMTimeToLiveMax;
}
payload.timeToLive = timeToLive;
}
return payload;
}
/**
* Slice a list of devices to several list of devices with fixed chunk size.
* @param {Array} devices An array of devices
* @param {Number} chunkSize The size of the a chunk
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
*/
function sliceDevices(devices, chunkSize) {
const chunkDevices = [];
while (devices.length > 0) {
chunkDevices.push(devices.splice(0, chunkSize));
}
return chunkDevices;
}
GCM.generateGCMPayload = generateGCMPayload;
/* istanbul ignore else */
if (process.env.TESTING) {
GCM.sliceDevices = sliceDevices;
}