@parse/push-adapter 6.4.0 → 6.5.0
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/README.md +19 -0
- package/package.json +7 -3
- package/src/APNS.js +76 -78
- package/src/EXPO.js +13 -13
- package/src/FCM.js +78 -66
- 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/README.md
CHANGED
|
@@ -19,8 +19,10 @@ The official Push Notification adapter for Parse Server. See [Parse Server Push
|
|
|
19
19
|
- [Configure Parse Server](#configure-parse-server)
|
|
20
20
|
- [Apple Push Options](#apple-push-options)
|
|
21
21
|
- [Android Push Options](#android-push-options)
|
|
22
|
+
- [Firebase Cloud Messaging (FCM)](#firebase-cloud-messaging-fcm)
|
|
22
23
|
- [Google Cloud Service Account Key](#google-cloud-service-account-key)
|
|
23
24
|
- [Migration to FCM HTTP v1 API (June 2024)](#migration-to-fcm-http-v1-api-june-2024)
|
|
25
|
+
- [HTTP/1.1 Legacy Option](#http11-legacy-option)
|
|
24
26
|
- [Expo Push Options](#expo-push-options)
|
|
25
27
|
- [Bundled with Parse Server](#bundled-with-parse-server)
|
|
26
28
|
- [Logging](#logging)
|
|
@@ -110,6 +112,10 @@ android: {
|
|
|
110
112
|
}
|
|
111
113
|
```
|
|
112
114
|
|
|
115
|
+
### Firebase Cloud Messaging (FCM)
|
|
116
|
+
|
|
117
|
+
This section contains some considerations when using FCM, regardless of the destination ecosystems the push notification is sent to.
|
|
118
|
+
|
|
113
119
|
#### Google Cloud Service Account Key
|
|
114
120
|
|
|
115
121
|
The Firebase console allows to easily create and download a Google Cloud service account key JSON file with the required permissions. Instead of setting `firebaseServiceAccount` to the path of the JSON file, you can provide an object representing a Google Cloud service account key:
|
|
@@ -139,6 +145,19 @@ android: {
|
|
|
139
145
|
}
|
|
140
146
|
```
|
|
141
147
|
|
|
148
|
+
#### HTTP/1.1 Legacy Option
|
|
149
|
+
|
|
150
|
+
With the introduction of the FCM HTTP v1 API, support for HTTP/2 was added which provides faster throughput for push notifications. To use the older version HTTP/1.1 set `fcmEnableLegacyHttpTransport: true` in your push options.
|
|
151
|
+
|
|
152
|
+
Example options:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
android: {
|
|
156
|
+
firebaseServiceAccount: __dirname + '/firebase.json',
|
|
157
|
+
fcmEnableLegacyHttpTransport: true
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
142
161
|
### Expo Push Options
|
|
143
162
|
|
|
144
163
|
Example options:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parse/push-adapter",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.0",
|
|
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": [
|
|
@@ -25,12 +27,13 @@
|
|
|
25
27
|
"@parse/node-apn": "6.0.1",
|
|
26
28
|
"@parse/node-gcm": "1.0.2",
|
|
27
29
|
"expo-server-sdk": "3.10.0",
|
|
28
|
-
"firebase-admin": "12.
|
|
30
|
+
"firebase-admin": "12.3.0",
|
|
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
|
@@ -25,14 +25,26 @@ export default function FCM(args, pushType) {
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const fcmEnableLegacyHttpTransport = typeof args.fcmEnableLegacyHttpTransport === 'boolean'
|
|
29
|
+
? args.fcmEnableLegacyHttpTransport
|
|
30
|
+
: false;
|
|
31
|
+
|
|
28
32
|
let app;
|
|
29
33
|
if (getApps().length === 0) {
|
|
30
34
|
app = initializeApp({ credential: cert(args.firebaseServiceAccount) });
|
|
31
35
|
} else {
|
|
32
36
|
app = getApp();
|
|
33
37
|
}
|
|
38
|
+
|
|
34
39
|
this.sender = getMessaging(app);
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
if (fcmEnableLegacyHttpTransport) {
|
|
42
|
+
this.sender.enableLegacyHttpTransport();
|
|
43
|
+
log.warn(LOG_PREFIX, 'Legacy HTTP/1.1 transport is enabled. This is a deprecated feature and support for this flag will be removed in the future.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Push type is only used to remain backwards compatible with APNS and GCM
|
|
47
|
+
this.pushType = pushType;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
FCM.FCMRegistrationTokensMax = FCMRegistrationTokensMax;
|
|
@@ -142,14 +154,14 @@ function _APNSToFCMPayload(requestData) {
|
|
|
142
154
|
coreData = requestData.data;
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
|
|
157
|
+
const expirationTime =
|
|
146
158
|
requestData['expiration_time'] || coreData['expiration_time'];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
const collapseId = requestData['collapse_id'] || coreData['collapse_id'];
|
|
160
|
+
const pushType = requestData['push_type'] || coreData['push_type'];
|
|
161
|
+
const priority = requestData['priority'] || coreData['priority'];
|
|
150
162
|
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
const apnsPayload = { apns: { payload: { aps: {} } } };
|
|
164
|
+
const headers = {};
|
|
153
165
|
|
|
154
166
|
// Set to alert by default if not set explicitly
|
|
155
167
|
headers['apns-push-type'] = 'alert';
|
|
@@ -172,70 +184,70 @@ function _APNSToFCMPayload(requestData) {
|
|
|
172
184
|
apnsPayload.apns.headers = headers;
|
|
173
185
|
}
|
|
174
186
|
|
|
175
|
-
for (
|
|
187
|
+
for (const key in coreData) {
|
|
176
188
|
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
|
-
|
|
189
|
+
case 'aps':
|
|
190
|
+
apnsPayload['apns']['payload']['aps'] = coreData.aps;
|
|
191
|
+
break;
|
|
192
|
+
case 'alert':
|
|
193
|
+
if (typeof coreData.alert == 'object') {
|
|
194
|
+
// When we receive a dictionary, use as is to remain
|
|
195
|
+
// compatible with how the APNS.js + node-apn work
|
|
196
|
+
apnsPayload['apns']['payload']['aps']['alert'] = coreData.alert;
|
|
197
|
+
} else {
|
|
198
|
+
// When we receive a value, prepare `alert` dictionary
|
|
199
|
+
// and set its `body` property
|
|
200
|
+
apnsPayload['apns']['payload']['aps']['alert'] = {};
|
|
201
|
+
apnsPayload['apns']['payload']['aps']['alert']['body'] = coreData.alert;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case 'title':
|
|
205
|
+
// Ensure the alert object exists before trying to assign the title
|
|
206
|
+
// title always goes into the nested `alert` dictionary
|
|
207
|
+
if (!apnsPayload['apns']['payload']['aps'].hasOwnProperty('alert')) {
|
|
208
|
+
apnsPayload['apns']['payload']['aps']['alert'] = {};
|
|
209
|
+
}
|
|
210
|
+
apnsPayload['apns']['payload']['aps']['alert']['title'] = coreData.title;
|
|
211
|
+
break;
|
|
212
|
+
case 'badge':
|
|
213
|
+
apnsPayload['apns']['payload']['aps']['badge'] = coreData.badge;
|
|
214
|
+
break;
|
|
215
|
+
case 'sound':
|
|
216
|
+
apnsPayload['apns']['payload']['aps']['sound'] = coreData.sound;
|
|
217
|
+
break;
|
|
218
|
+
case 'content-available':
|
|
219
|
+
apnsPayload['apns']['payload']['aps']['content-available'] =
|
|
208
220
|
coreData['content-available'];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
break;
|
|
222
|
+
case 'mutable-content':
|
|
223
|
+
apnsPayload['apns']['payload']['aps']['mutable-content'] =
|
|
212
224
|
coreData['mutable-content'];
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
break;
|
|
226
|
+
case 'targetContentIdentifier':
|
|
227
|
+
apnsPayload['apns']['payload']['aps']['target-content-id'] =
|
|
216
228
|
coreData.targetContentIdentifier;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
break;
|
|
230
|
+
case 'interruptionLevel':
|
|
231
|
+
apnsPayload['apns']['payload']['aps']['interruption-level'] =
|
|
220
232
|
coreData.interruptionLevel;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
233
|
+
break;
|
|
234
|
+
case 'category':
|
|
235
|
+
apnsPayload['apns']['payload']['aps']['category'] = coreData.category;
|
|
236
|
+
break;
|
|
237
|
+
case 'threadId':
|
|
238
|
+
apnsPayload['apns']['payload']['aps']['thread-id'] = coreData.threadId;
|
|
239
|
+
break;
|
|
240
|
+
case 'expiration_time': // Exclude header-related fields as these are set above
|
|
241
|
+
break;
|
|
242
|
+
case 'collapse_id':
|
|
243
|
+
break;
|
|
244
|
+
case 'push_type':
|
|
245
|
+
break;
|
|
246
|
+
case 'priority':
|
|
247
|
+
break;
|
|
248
|
+
default:
|
|
249
|
+
apnsPayload['apns']['payload'][key] = coreData[key]; // Custom keys should be outside aps
|
|
250
|
+
break;
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
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';
|