@parse/push-adapter 6.4.0 → 6.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/src/APNS.js +76 -78
- package/src/EXPO.js +13 -13
- package/src/FCM.js +65 -65
- package/src/GCM.js +25 -25
- package/src/ParsePushAdapter.js +33 -33
- package/src/PushAdapterUtils.js +6 -6
- package/src/WEB.js +4 -4
- package/src/index.js +4 -2
- package/src/utils.js +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parse/push-adapter",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.1",
|
|
4
4
|
"description": "Base parse-server-push-adapter",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"src/"
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
|
+
"lint": "eslint --cache ./",
|
|
12
|
+
"lint:fix": "eslint --fix --cache ./",
|
|
11
13
|
"test": "TESTING=1 c8 ./node_modules/.bin/jasmine"
|
|
12
14
|
},
|
|
13
15
|
"keywords": [
|
|
@@ -27,10 +29,11 @@
|
|
|
27
29
|
"expo-server-sdk": "3.10.0",
|
|
28
30
|
"firebase-admin": "12.1.1",
|
|
29
31
|
"npmlog": "7.0.1",
|
|
30
|
-
"parse": "5.
|
|
32
|
+
"parse": "5.2.0",
|
|
31
33
|
"web-push": "3.6.7"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
36
|
+
"@eslint/js": "9.6.0",
|
|
34
37
|
"@semantic-release/changelog": "6.0.3",
|
|
35
38
|
"@semantic-release/commit-analyzer": "13.0.0",
|
|
36
39
|
"@semantic-release/git": "10.0.1",
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
"@semantic-release/release-notes-generator": "14.0.1",
|
|
40
43
|
"c8": "10.1.2",
|
|
41
44
|
"codecov": "3.8.0",
|
|
45
|
+
"eslint": "9.6.0",
|
|
42
46
|
"jasmine": "5.1.0",
|
|
43
47
|
"jasmine-spec-reporter": "7.0.0",
|
|
44
48
|
"semantic-release": "24.0.0"
|
package/src/APNS.js
CHANGED
|
@@ -39,7 +39,7 @@ export class APNS {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Create Provider from each arg-object
|
|
42
|
-
for (
|
|
42
|
+
for (const apnsArgs of apnsArgsList) {
|
|
43
43
|
|
|
44
44
|
// rewrite bundleId to topic for backward-compatibility
|
|
45
45
|
if (apnsArgs.bundleId) {
|
|
@@ -47,7 +47,7 @@ export class APNS {
|
|
|
47
47
|
apnsArgs.topic = apnsArgs.bundleId
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
const provider = APNS._createProvider(apnsArgs);
|
|
51
51
|
this.providers.push(provider);
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -70,42 +70,42 @@ export class APNS {
|
|
|
70
70
|
* @returns {Object} A promise which is resolved immediately
|
|
71
71
|
*/
|
|
72
72
|
send(data, allDevices) {
|
|
73
|
-
|
|
73
|
+
const coreData = data && data.data;
|
|
74
74
|
if (!coreData || !allDevices || !Array.isArray(allDevices)) {
|
|
75
75
|
log.warn(LOG_PREFIX, 'invalid push payload');
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
const expirationTime = data['expiration_time'] || coreData['expiration_time'];
|
|
79
|
+
const collapseId = data['collapse_id'] || coreData['collapse_id'];
|
|
80
|
+
const pushType = data['push_type'] || coreData['push_type'];
|
|
81
|
+
const priority = data['priority'] || coreData['priority'];
|
|
82
82
|
let allPromises = [];
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
const devicesPerAppIdentifier = {};
|
|
85
85
|
|
|
86
86
|
// Start by clustering the devices per appIdentifier
|
|
87
87
|
allDevices.forEach(device => {
|
|
88
|
-
|
|
88
|
+
const appIdentifier = device.appIdentifier;
|
|
89
89
|
devicesPerAppIdentifier[appIdentifier] = devicesPerAppIdentifier[appIdentifier] || [];
|
|
90
90
|
devicesPerAppIdentifier[appIdentifier].push(device);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
for (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
for (const key in devicesPerAppIdentifier) {
|
|
94
|
+
const devices = devicesPerAppIdentifier[key];
|
|
95
|
+
const appIdentifier = devices[0].appIdentifier;
|
|
96
|
+
const providers = this._chooseProviders(appIdentifier);
|
|
97
97
|
|
|
98
98
|
// No Providers found
|
|
99
99
|
if (!providers || providers.length === 0) {
|
|
100
|
-
|
|
100
|
+
const errorPromises = devices.map(device => APNS._createErrorPromise(device.deviceToken, 'No Provider found'));
|
|
101
101
|
allPromises = allPromises.concat(errorPromises);
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
const headers = { expirationTime: expirationTime, topic: appIdentifier, collapseId: collapseId, pushType: pushType, priority: priority }
|
|
106
|
+
const notification = APNS._generateNotification(coreData, headers);
|
|
107
107
|
const deviceIds = devices.map(device => device.deviceToken);
|
|
108
|
-
|
|
108
|
+
const promise = this.sendThroughProvider(notification, deviceIds, providers);
|
|
109
109
|
allPromises.push(promise.then(this._handlePromise.bind(this)));
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -117,25 +117,25 @@ export class APNS {
|
|
|
117
117
|
|
|
118
118
|
sendThroughProvider(notification, devices, providers) {
|
|
119
119
|
return providers[0]
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
.send(notification, devices)
|
|
121
|
+
.then((response) => {
|
|
122
|
+
if (response.failed
|
|
123
123
|
&& response.failed.length > 0
|
|
124
124
|
&& providers && providers.length > 1) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return response;
|
|
134
|
-
});
|
|
135
|
-
} else {
|
|
125
|
+
const devices = response.failed.map((failure) => { return failure.device; });
|
|
126
|
+
// Reset the failures as we'll try next connection
|
|
127
|
+
response.failed = [];
|
|
128
|
+
return this.sendThroughProvider(notification,
|
|
129
|
+
devices,
|
|
130
|
+
providers.slice(1, providers.length)).then((retryResponse) => {
|
|
131
|
+
response.failed = response.failed.concat(retryResponse.failed);
|
|
132
|
+
response.sent = response.sent.concat(retryResponse.sent);
|
|
136
133
|
return response;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
static _validateAPNArgs(apnsArgs) {
|
|
@@ -154,7 +154,7 @@ export class APNS {
|
|
|
154
154
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is mssing for %j', apnsArgs);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
const provider = new apn.Provider(apnsArgs);
|
|
158
158
|
|
|
159
159
|
// Sets the topic on this provider
|
|
160
160
|
provider.topic = apnsArgs.topic;
|
|
@@ -176,48 +176,46 @@ export class APNS {
|
|
|
176
176
|
* @returns {Object} A apns Notification
|
|
177
177
|
*/
|
|
178
178
|
static _generateNotification(coreData, headers) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
for (
|
|
179
|
+
const notification = new apn.Notification();
|
|
180
|
+
const payload = {};
|
|
181
|
+
for (const key in coreData) {
|
|
182
182
|
switch (key) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
183
|
+
case 'aps':
|
|
184
|
+
notification.aps = coreData.aps;
|
|
185
|
+
break;
|
|
186
|
+
case 'alert':
|
|
187
|
+
notification.setAlert(coreData.alert);
|
|
188
|
+
break;
|
|
189
|
+
case 'title':
|
|
190
|
+
notification.setTitle(coreData.title);
|
|
191
|
+
break;
|
|
192
|
+
case 'badge':
|
|
193
|
+
notification.setBadge(coreData.badge);
|
|
194
|
+
break;
|
|
195
|
+
case 'sound':
|
|
196
|
+
notification.setSound(coreData.sound);
|
|
197
|
+
break;
|
|
198
|
+
case 'content-available':
|
|
199
|
+
notification.setContentAvailable(coreData['content-available'] === 1);
|
|
200
|
+
break;
|
|
201
|
+
case 'mutable-content':
|
|
202
|
+
notification.setMutableContent(coreData['mutable-content'] === 1);
|
|
203
|
+
break;
|
|
204
|
+
case 'targetContentIdentifier':
|
|
205
|
+
notification.setTargetContentIdentifier(coreData.targetContentIdentifier);
|
|
206
|
+
break;
|
|
207
|
+
case 'interruptionLevel':
|
|
208
|
+
notification.setInterruptionLevel(coreData.interruptionLevel);
|
|
209
|
+
break;
|
|
210
|
+
case 'category':
|
|
211
|
+
notification.setCategory(coreData.category);
|
|
212
|
+
break;
|
|
213
|
+
case 'threadId':
|
|
214
|
+
notification.setThreadId(coreData.threadId);
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
payload[key] = coreData[key];
|
|
191
218
|
break;
|
|
192
|
-
case 'badge':
|
|
193
|
-
notification.setBadge(coreData.badge);
|
|
194
|
-
break;
|
|
195
|
-
case 'sound':
|
|
196
|
-
notification.setSound(coreData.sound);
|
|
197
|
-
break;
|
|
198
|
-
case 'content-available':
|
|
199
|
-
let isAvailable = coreData['content-available'] === 1;
|
|
200
|
-
notification.setContentAvailable(isAvailable);
|
|
201
|
-
break;
|
|
202
|
-
case 'mutable-content':
|
|
203
|
-
let isMutable = coreData['mutable-content'] === 1;
|
|
204
|
-
notification.setMutableContent(isMutable);
|
|
205
|
-
break;
|
|
206
|
-
case 'targetContentIdentifier':
|
|
207
|
-
notification.setTargetContentIdentifier(coreData.targetContentIdentifier);
|
|
208
|
-
break;
|
|
209
|
-
case 'interruptionLevel':
|
|
210
|
-
notification.setInterruptionLevel(coreData.interruptionLevel);
|
|
211
|
-
break;
|
|
212
|
-
case 'category':
|
|
213
|
-
notification.setCategory(coreData.category);
|
|
214
|
-
break;
|
|
215
|
-
case 'threadId':
|
|
216
|
-
notification.setThreadId(coreData.threadId);
|
|
217
|
-
break;
|
|
218
|
-
default:
|
|
219
|
-
payload[key] = coreData[key];
|
|
220
|
-
break;
|
|
221
219
|
}
|
|
222
220
|
}
|
|
223
221
|
|
|
@@ -251,7 +249,7 @@ export class APNS {
|
|
|
251
249
|
}*/
|
|
252
250
|
|
|
253
251
|
// Otherwise we try to match the appIdentifier with topic on provider
|
|
254
|
-
|
|
252
|
+
const qualifiedProviders = this.providers.filter((provider) => appIdentifier === provider.topic);
|
|
255
253
|
|
|
256
254
|
if (qualifiedProviders.length > 0) {
|
|
257
255
|
return qualifiedProviders;
|
|
@@ -263,7 +261,7 @@ export class APNS {
|
|
|
263
261
|
}
|
|
264
262
|
|
|
265
263
|
_handlePromise(response) {
|
|
266
|
-
|
|
264
|
+
const promises = [];
|
|
267
265
|
response.sent.forEach((token) => {
|
|
268
266
|
log.verbose(LOG_PREFIX, 'APNS transmitted to %s', token.device);
|
|
269
267
|
promises.push(APNS._createSuccesfullPromise(token.device));
|
|
@@ -282,8 +280,8 @@ export class APNS {
|
|
|
282
280
|
log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason);
|
|
283
281
|
return APNS._createErrorPromise(failure.device, failure.response.reason);
|
|
284
282
|
} else {
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
log.error(LOG_PREFIX, 'APNS error transmitting to device with unkown error');
|
|
284
|
+
return APNS._createErrorPromise(failure.device, 'Unkown status');
|
|
287
285
|
}
|
|
288
286
|
}
|
|
289
287
|
|
package/src/EXPO.js
CHANGED
|
@@ -7,19 +7,19 @@ import { Expo } from 'expo-server-sdk';
|
|
|
7
7
|
const LOG_PREFIX = 'parse-server-push-adapter EXPO';
|
|
8
8
|
|
|
9
9
|
function expoResultToParseResponse(result) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
10
|
+
if (result.status === 'ok') {
|
|
11
|
+
return result;
|
|
12
|
+
} else {
|
|
13
|
+
// ParseServer looks for "error", and supports ceratin codes like 'NotRegistered' for
|
|
14
|
+
// cleanup. Expo returns slighyly different ones so changing to match what is expected
|
|
15
|
+
// This can be taken out if the responsibility gets moved to the adapter itself.
|
|
16
|
+
const error = result.message === 'DeviceNotRegistered' ?
|
|
17
|
+
'NotRegistered' : result.message;
|
|
18
|
+
return {
|
|
19
|
+
error,
|
|
20
|
+
...result
|
|
22
21
|
}
|
|
22
|
+
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export class EXPO {
|
|
@@ -60,7 +60,7 @@ export class EXPO {
|
|
|
60
60
|
|
|
61
61
|
const resolvers = [];
|
|
62
62
|
const promises = deviceTokens.map(() => new Promise(resolve => resolvers.push(resolve)));
|
|
63
|
-
|
|
63
|
+
const length = deviceTokens.length;
|
|
64
64
|
|
|
65
65
|
log.verbose(LOG_PREFIX, `sending to ${length} ${length > 1 ? 'devices' : 'device'}`);
|
|
66
66
|
|
package/src/FCM.js
CHANGED
|
@@ -142,14 +142,14 @@ function _APNSToFCMPayload(requestData) {
|
|
|
142
142
|
coreData = requestData.data;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
const expirationTime =
|
|
146
146
|
requestData['expiration_time'] || coreData['expiration_time'];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
const collapseId = requestData['collapse_id'] || coreData['collapse_id'];
|
|
148
|
+
const pushType = requestData['push_type'] || coreData['push_type'];
|
|
149
|
+
const priority = requestData['priority'] || coreData['priority'];
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
const apnsPayload = { apns: { payload: { aps: {} } } };
|
|
152
|
+
const headers = {};
|
|
153
153
|
|
|
154
154
|
// Set to alert by default if not set explicitly
|
|
155
155
|
headers['apns-push-type'] = 'alert';
|
|
@@ -172,70 +172,70 @@ function _APNSToFCMPayload(requestData) {
|
|
|
172
172
|
apnsPayload.apns.headers = headers;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
for (
|
|
175
|
+
for (const key in coreData) {
|
|
176
176
|
switch (key) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
177
|
+
case 'aps':
|
|
178
|
+
apnsPayload['apns']['payload']['aps'] = coreData.aps;
|
|
179
|
+
break;
|
|
180
|
+
case 'alert':
|
|
181
|
+
if (typeof coreData.alert == 'object') {
|
|
182
|
+
// When we receive a dictionary, use as is to remain
|
|
183
|
+
// compatible with how the APNS.js + node-apn work
|
|
184
|
+
apnsPayload['apns']['payload']['aps']['alert'] = coreData.alert;
|
|
185
|
+
} else {
|
|
186
|
+
// When we receive a value, prepare `alert` dictionary
|
|
187
|
+
// and set its `body` property
|
|
188
|
+
apnsPayload['apns']['payload']['aps']['alert'] = {};
|
|
189
|
+
apnsPayload['apns']['payload']['aps']['alert']['body'] = coreData.alert;
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
case 'title':
|
|
193
|
+
// Ensure the alert object exists before trying to assign the title
|
|
194
|
+
// title always goes into the nested `alert` dictionary
|
|
195
|
+
if (!apnsPayload['apns']['payload']['aps'].hasOwnProperty('alert')) {
|
|
196
|
+
apnsPayload['apns']['payload']['aps']['alert'] = {};
|
|
197
|
+
}
|
|
198
|
+
apnsPayload['apns']['payload']['aps']['alert']['title'] = coreData.title;
|
|
199
|
+
break;
|
|
200
|
+
case 'badge':
|
|
201
|
+
apnsPayload['apns']['payload']['aps']['badge'] = coreData.badge;
|
|
202
|
+
break;
|
|
203
|
+
case 'sound':
|
|
204
|
+
apnsPayload['apns']['payload']['aps']['sound'] = coreData.sound;
|
|
205
|
+
break;
|
|
206
|
+
case 'content-available':
|
|
207
|
+
apnsPayload['apns']['payload']['aps']['content-available'] =
|
|
208
208
|
coreData['content-available'];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
break;
|
|
210
|
+
case 'mutable-content':
|
|
211
|
+
apnsPayload['apns']['payload']['aps']['mutable-content'] =
|
|
212
212
|
coreData['mutable-content'];
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
break;
|
|
214
|
+
case 'targetContentIdentifier':
|
|
215
|
+
apnsPayload['apns']['payload']['aps']['target-content-id'] =
|
|
216
216
|
coreData.targetContentIdentifier;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
break;
|
|
218
|
+
case 'interruptionLevel':
|
|
219
|
+
apnsPayload['apns']['payload']['aps']['interruption-level'] =
|
|
220
220
|
coreData.interruptionLevel;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
221
|
+
break;
|
|
222
|
+
case 'category':
|
|
223
|
+
apnsPayload['apns']['payload']['aps']['category'] = coreData.category;
|
|
224
|
+
break;
|
|
225
|
+
case 'threadId':
|
|
226
|
+
apnsPayload['apns']['payload']['aps']['thread-id'] = coreData.threadId;
|
|
227
|
+
break;
|
|
228
|
+
case 'expiration_time': // Exclude header-related fields as these are set above
|
|
229
|
+
break;
|
|
230
|
+
case 'collapse_id':
|
|
231
|
+
break;
|
|
232
|
+
case 'push_type':
|
|
233
|
+
break;
|
|
234
|
+
case 'priority':
|
|
235
|
+
break;
|
|
236
|
+
default:
|
|
237
|
+
apnsPayload['apns']['payload'][key] = coreData[key]; // Custom keys should be outside aps
|
|
238
|
+
break;
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
return apnsPayload;
|
package/src/GCM.js
CHANGED
|
@@ -12,7 +12,7 @@ const GCMRegistrationTokensMax = 1000;
|
|
|
12
12
|
export default function GCM(args) {
|
|
13
13
|
if (typeof args !== 'object' || !args.apiKey) {
|
|
14
14
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
|
15
|
-
|
|
15
|
+
'GCM Configuration is invalid');
|
|
16
16
|
}
|
|
17
17
|
this.sender = new gcm.Sender(args.apiKey, args.requestOptions);
|
|
18
18
|
}
|
|
@@ -30,23 +30,23 @@ GCM.prototype.send = function(data, devices) {
|
|
|
30
30
|
log.warn(LOG_PREFIX, 'invalid push payload');
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
const pushId = randomString(10);
|
|
34
34
|
// Make a new array
|
|
35
|
-
devices=devices.slice(0);
|
|
36
|
-
|
|
35
|
+
devices = devices.slice(0);
|
|
36
|
+
const timestamp = Date.now();
|
|
37
37
|
// For android, we can only have 1000 recepients per send, so we need to slice devices to
|
|
38
38
|
// chunk if necessary
|
|
39
|
-
|
|
39
|
+
const slices = sliceDevices(devices, GCM.GCMRegistrationTokensMax);
|
|
40
40
|
if (slices.length > 1) {
|
|
41
41
|
log.verbose(LOG_PREFIX, `the number of devices exceeds ${GCMRegistrationTokensMax}`);
|
|
42
42
|
// Make 1 send per slice
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const promises = slices.reduce((memo, slice) => {
|
|
44
|
+
const promise = this.send(data, slice, timestamp);
|
|
45
45
|
memo.push(promise);
|
|
46
46
|
return memo;
|
|
47
47
|
}, [])
|
|
48
|
-
return Promise.all(promises).then((results) =>
|
|
49
|
-
|
|
48
|
+
return Promise.all(promises).then((results) => {
|
|
49
|
+
const allResults = results.reduce((memo, result) => {
|
|
50
50
|
return memo.concat(result);
|
|
51
51
|
}, []);
|
|
52
52
|
return Promise.resolve(allResults);
|
|
@@ -63,23 +63,23 @@ GCM.prototype.send = function(data, devices) {
|
|
|
63
63
|
}
|
|
64
64
|
// Generate gcm payload
|
|
65
65
|
// PushId is not a formal field of GCM, but Parse Android SDK uses this field to deduplicate push notifications
|
|
66
|
-
|
|
66
|
+
const gcmPayload = generateGCMPayload(data, pushId, timestamp, expirationTime);
|
|
67
67
|
// Make and send gcm request
|
|
68
|
-
|
|
68
|
+
const message = new gcm.Message(gcmPayload);
|
|
69
69
|
|
|
70
70
|
// Build a device map
|
|
71
|
-
|
|
71
|
+
const devicesMap = devices.reduce((memo, device) => {
|
|
72
72
|
memo[device.deviceToken] = device;
|
|
73
73
|
return memo;
|
|
74
74
|
}, {});
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
const deviceTokens = Object.keys(devicesMap);
|
|
77
77
|
|
|
78
78
|
const resolvers = [];
|
|
79
|
-
const promises = deviceTokens.map(() =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
log.verbose(LOG_PREFIX, `sending to ${length} ${length >
|
|
79
|
+
const promises = deviceTokens.map(() => new Promise(resolve => resolvers.push(resolve)));
|
|
80
|
+
const registrationTokens = deviceTokens;
|
|
81
|
+
const length = registrationTokens.length;
|
|
82
|
+
log.verbose(LOG_PREFIX, `sending to ${length} ${length > 1 ? 'devices' : 'device'}`);
|
|
83
83
|
this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
|
|
84
84
|
// example response:
|
|
85
85
|
/*
|
|
@@ -97,13 +97,13 @@ GCM.prototype.send = function(data, devices) {
|
|
|
97
97
|
} else {
|
|
98
98
|
log.verbose(LOG_PREFIX, `GCM Response: %s`, JSON.stringify(response, null, 4));
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
const { results, multicast_id } = response || {};
|
|
101
101
|
registrationTokens.forEach((token, index) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const resolve = resolvers[index];
|
|
103
|
+
const result = results ? results[index] : undefined;
|
|
104
|
+
const device = devicesMap[token];
|
|
105
105
|
device.deviceType = 'android';
|
|
106
|
-
|
|
106
|
+
const resolution = {
|
|
107
107
|
device,
|
|
108
108
|
multicast_id,
|
|
109
109
|
response: error || result,
|
|
@@ -128,7 +128,7 @@ GCM.prototype.send = function(data, devices) {
|
|
|
128
128
|
* @returns {Object} A promise which is resolved after we get results from gcm
|
|
129
129
|
*/
|
|
130
130
|
function generateGCMPayload(requestData, pushId, timeStamp, expirationTime) {
|
|
131
|
-
|
|
131
|
+
const payload = {
|
|
132
132
|
priority: 'high'
|
|
133
133
|
};
|
|
134
134
|
payload.data = {
|
|
@@ -144,7 +144,7 @@ function generateGCMPayload(requestData, pushId, timeStamp, expirationTime) {
|
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
if (expirationTime) {
|
|
147
|
-
|
|
147
|
+
// The timeStamp and expiration is in milliseconds but gcm requires second
|
|
148
148
|
let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
|
|
149
149
|
if (timeToLive < 0) {
|
|
150
150
|
timeToLive = 0;
|
|
@@ -164,7 +164,7 @@ function generateGCMPayload(requestData, pushId, timeStamp, expirationTime) {
|
|
|
164
164
|
* @returns {Array} An array which contaisn several arries of devices with fixed chunk size
|
|
165
165
|
*/
|
|
166
166
|
function sliceDevices(devices, chunkSize) {
|
|
167
|
-
|
|
167
|
+
const chunkDevices = [];
|
|
168
168
|
while (devices.length > 0) {
|
|
169
169
|
chunkDevices.push(devices.splice(0, chunkSize));
|
|
170
170
|
}
|
package/src/ParsePushAdapter.js
CHANGED
|
@@ -21,38 +21,38 @@ export default class ParsePushAdapter {
|
|
|
21
21
|
this.feature = {
|
|
22
22
|
immediatePush: true
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
const pushTypes = Object.keys(pushConfig);
|
|
25
25
|
|
|
26
|
-
for (
|
|
26
|
+
for (const pushType of pushTypes) {
|
|
27
27
|
// adapter may be passed as part of the parse-server initialization
|
|
28
28
|
if (this.validPushTypes.indexOf(pushType) < 0 && pushType != 'adapter') {
|
|
29
29
|
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
|
30
|
-
|
|
30
|
+
'Push to ' + pushType + ' is not supported');
|
|
31
31
|
}
|
|
32
32
|
switch (pushType) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
33
|
+
case 'ios':
|
|
34
|
+
case 'tvos':
|
|
35
|
+
case 'osx':
|
|
36
|
+
if (pushConfig[pushType].hasOwnProperty('firebaseServiceAccount')) {
|
|
37
|
+
this.senderMap[pushType] = new FCM(pushConfig[pushType], 'apple');
|
|
38
|
+
} else {
|
|
39
|
+
this.senderMap[pushType] = new APNS(pushConfig[pushType]);
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case 'web':
|
|
43
|
+
this.senderMap[pushType] = new WEB(pushConfig[pushType]);
|
|
44
|
+
break;
|
|
45
|
+
case 'expo':
|
|
46
|
+
this.senderMap[pushType] = new EXPO(pushConfig[pushType]);
|
|
47
|
+
break;
|
|
48
|
+
case 'android':
|
|
49
|
+
case 'fcm':
|
|
50
|
+
if (pushConfig[pushType].hasOwnProperty('firebaseServiceAccount')) {
|
|
51
|
+
this.senderMap[pushType] = new FCM(pushConfig[pushType], 'android');
|
|
52
|
+
} else {
|
|
53
|
+
this.senderMap[pushType] = new GCM(pushConfig[pushType]);
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -66,16 +66,16 @@ export default class ParsePushAdapter {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
send(data, installations) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for (
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
const deviceMap = classifyInstallations(installations, this.validPushTypes);
|
|
70
|
+
const sendPromises = [];
|
|
71
|
+
for (const pushType in deviceMap) {
|
|
72
|
+
const sender = this.senderMap[pushType];
|
|
73
|
+
const devices = deviceMap[pushType];
|
|
74
74
|
|
|
75
75
|
if(Array.isArray(devices) && devices.length > 0) {
|
|
76
76
|
if (!sender) {
|
|
77
77
|
log.verbose(LOG_PREFIX, `Can not find sender for push type ${pushType}, ${data}`)
|
|
78
|
-
|
|
78
|
+
const results = devices.map((device) => {
|
|
79
79
|
return Promise.resolve({
|
|
80
80
|
device,
|
|
81
81
|
transmitted: false,
|
|
@@ -88,7 +88,7 @@ export default class ParsePushAdapter {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
return Promise.all(sendPromises).then((promises) =>
|
|
91
|
+
return Promise.all(sendPromises).then((promises) => {
|
|
92
92
|
// flatten all
|
|
93
93
|
return [].concat.apply([], promises);
|
|
94
94
|
})
|
package/src/PushAdapterUtils.js
CHANGED
|
@@ -8,16 +8,16 @@ import { randomBytes } from 'crypto';
|
|
|
8
8
|
*/
|
|
9
9
|
export function classifyInstallations(installations, validPushTypes) {
|
|
10
10
|
// Init deviceTokenMap, create a empty array for each valid pushType
|
|
11
|
-
|
|
12
|
-
for (
|
|
11
|
+
const deviceMap = {};
|
|
12
|
+
for (const validPushType of validPushTypes) {
|
|
13
13
|
deviceMap[validPushType] = [];
|
|
14
14
|
}
|
|
15
|
-
for (
|
|
15
|
+
for (const installation of installations) {
|
|
16
16
|
// No deviceToken, ignore
|
|
17
17
|
if (!installation.deviceToken) {
|
|
18
18
|
continue;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
const devices = deviceMap[installation.pushType] || deviceMap[installation.deviceType] || null;
|
|
21
21
|
if (Array.isArray(devices)) {
|
|
22
22
|
devices.push({
|
|
23
23
|
deviceToken: installation.deviceToken,
|
|
@@ -33,11 +33,11 @@ export function randomString(size) {
|
|
|
33
33
|
if (size === 0) {
|
|
34
34
|
throw new Error('Zero-length randomString is useless.');
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
const chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
|
|
37
37
|
'abcdefghijklmnopqrstuvwxyz' +
|
|
38
38
|
'0123456789');
|
|
39
39
|
let objectId = '';
|
|
40
|
-
|
|
40
|
+
const bytes = randomBytes(size);
|
|
41
41
|
for (let i = 0; i < bytes.length; ++i) {
|
|
42
42
|
objectId += chars[bytes.readUInt8(i) % chars.length];
|
|
43
43
|
}
|
package/src/WEB.js
CHANGED
|
@@ -9,7 +9,7 @@ const LOG_PREFIX = 'parse-server-push-adapter WEB';
|
|
|
9
9
|
export class WEB {
|
|
10
10
|
/**
|
|
11
11
|
* Create a new WEB push adapter.
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* @param {Object} args https://github.com/web-push-libs/web-push#api-reference
|
|
14
14
|
*/
|
|
15
15
|
constructor(args) {
|
|
@@ -39,9 +39,9 @@ export class WEB {
|
|
|
39
39
|
const deviceTokens = Object.keys(devicesMap);
|
|
40
40
|
|
|
41
41
|
const resolvers = [];
|
|
42
|
-
const promises = deviceTokens.map(() =>
|
|
43
|
-
|
|
44
|
-
log.verbose(LOG_PREFIX, `sending to ${length} ${length >
|
|
42
|
+
const promises = deviceTokens.map(() => new Promise(resolve => resolvers.push(resolve)));
|
|
43
|
+
const length = deviceTokens.length;
|
|
44
|
+
log.verbose(LOG_PREFIX, `sending to ${length} ${length > 1 ? 'devices' : 'device'}`);
|
|
45
45
|
|
|
46
46
|
const response = await WEB.sendNotifications(coreData, deviceTokens, this.options);
|
|
47
47
|
const { results, sent, failed } = response;
|
package/src/index.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
// PushAdapter, it uses GCM for android push, APNS for ios push.
|
|
4
4
|
// WEB for web push.
|
|
5
5
|
import log from 'npmlog';
|
|
6
|
+
import { booleanParser } from './utils.js';
|
|
6
7
|
|
|
7
|
-
/*
|
|
8
|
-
if (process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER_PUSH_ADAPTER) {
|
|
8
|
+
/* c8 ignore start */
|
|
9
|
+
if (booleanParser(process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER_PUSH_ADAPTER)) {
|
|
9
10
|
log.level = 'verbose';
|
|
10
11
|
}
|
|
12
|
+
/* c8 ignore stop */
|
|
11
13
|
|
|
12
14
|
import ParsePushAdapter from './ParsePushAdapter.js';
|
|
13
15
|
import GCM from './GCM.js';
|