@parse/push-adapter 6.2.0 → 6.3.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.
@@ -1,38 +1,20 @@
1
1
  "use strict";
2
2
 
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
-
7
- var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
8
-
9
- exports.default = GCM;
10
-
11
- var _parse = require('parse');
12
-
13
- var _parse2 = _interopRequireDefault(_parse);
14
-
15
- var _npmlog = require('npmlog');
16
-
17
- var _npmlog2 = _interopRequireDefault(_npmlog);
18
-
19
- var _nodeGcm = require('@parse/node-gcm');
20
-
21
- var _nodeGcm2 = _interopRequireDefault(_nodeGcm);
22
-
23
- var _PushAdapterUtils = require('./PushAdapterUtils');
24
-
25
- function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
26
-
27
- var LOG_PREFIX = 'parse-server-push-adapter GCM';
28
- var GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
29
- var GCMRegistrationTokensMax = 1000;
30
-
31
- function GCM(args) {
32
- if ((typeof args === 'undefined' ? 'undefined' : _typeof(args)) !== 'object' || !args.apiKey) {
33
- throw new _parse2.default.Error(_parse2.default.Error.PUSH_MISCONFIGURED, 'GCM Configuration is invalid');
3
+ import Parse from 'parse';
4
+ import log from 'npmlog';
5
+ import gcm from '@parse/node-gcm';
6
+ import { randomString } from './PushAdapterUtils.js';
7
+
8
+ const LOG_PREFIX = 'parse-server-push-adapter GCM';
9
+ const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
10
+ const GCMRegistrationTokensMax = 1000;
11
+
12
+ export default function GCM(args) {
13
+ if (typeof args !== 'object' || !args.apiKey) {
14
+ throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
15
+ 'GCM Configuration is invalid');
34
16
  }
35
- this.sender = new _nodeGcm2.default.Sender(args.apiKey, args.requestOptions);
17
+ this.sender = new gcm.Sender(args.apiKey, args.requestOptions);
36
18
  }
37
19
 
38
20
  GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;
@@ -43,30 +25,28 @@ GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax;
43
25
  * @param {Array} devices A array of devices
44
26
  * @returns {Object} A promise which is resolved after we get results from gcm
45
27
  */
46
- GCM.prototype.send = function (data, devices) {
47
- var _this = this;
48
-
28
+ GCM.prototype.send = function(data, devices) {
49
29
  if (!data || !devices || !Array.isArray(devices)) {
50
- _npmlog2.default.warn(LOG_PREFIX, 'invalid push payload');
30
+ log.warn(LOG_PREFIX, 'invalid push payload');
51
31
  return;
52
32
  }
53
- var pushId = (0, _PushAdapterUtils.randomString)(10);
33
+ let pushId = randomString(10);
54
34
  // Make a new array
55
- devices = devices.slice(0);
56
- var timestamp = Date.now();
35
+ devices=devices.slice(0);
36
+ let timestamp = Date.now();
57
37
  // For android, we can only have 1000 recepients per send, so we need to slice devices to
58
38
  // chunk if necessary
59
- var slices = sliceDevices(devices, GCM.GCMRegistrationTokensMax);
39
+ let slices = sliceDevices(devices, GCM.GCMRegistrationTokensMax);
60
40
  if (slices.length > 1) {
61
- _npmlog2.default.verbose(LOG_PREFIX, 'the number of devices exceeds ' + GCMRegistrationTokensMax);
41
+ log.verbose(LOG_PREFIX, `the number of devices exceeds ${GCMRegistrationTokensMax}`);
62
42
  // Make 1 send per slice
63
- var _promises = slices.reduce(function (memo, slice) {
64
- var promise = _this.send(data, slice, timestamp);
43
+ let promises = slices.reduce((memo, slice) => {
44
+ let promise = this.send(data, slice, timestamp);
65
45
  memo.push(promise);
66
46
  return memo;
67
- }, []);
68
- return Promise.all(_promises).then(function (results) {
69
- var allResults = results.reduce(function (memo, result) {
47
+ }, [])
48
+ return Promise.all(promises).then((results) => {
49
+ let allResults = results.reduce((memo, result) => {
70
50
  return memo.concat(result);
71
51
  }, []);
72
52
  return Promise.resolve(allResults);
@@ -75,7 +55,7 @@ GCM.prototype.send = function (data, devices) {
75
55
  // get the devices back...
76
56
  devices = slices[0];
77
57
 
78
- var expirationTime = void 0;
58
+ let expirationTime;
79
59
  // We handle the expiration_time convertion in push.js, so expiration_time is a valid date
80
60
  // in Unix epoch time in milliseconds here
81
61
  if (data['expiration_time']) {
@@ -83,28 +63,24 @@ GCM.prototype.send = function (data, devices) {
83
63
  }
84
64
  // Generate gcm payload
85
65
  // PushId is not a formal field of GCM, but Parse Android SDK uses this field to deduplicate push notifications
86
- var gcmPayload = generateGCMPayload(data, pushId, timestamp, expirationTime);
66
+ let gcmPayload = generateGCMPayload(data, pushId, timestamp, expirationTime);
87
67
  // Make and send gcm request
88
- var message = new _nodeGcm2.default.Message(gcmPayload);
68
+ let message = new gcm.Message(gcmPayload);
89
69
 
90
70
  // Build a device map
91
- var devicesMap = devices.reduce(function (memo, device) {
71
+ let devicesMap = devices.reduce((memo, device) => {
92
72
  memo[device.deviceToken] = device;
93
73
  return memo;
94
74
  }, {});
95
75
 
96
- var deviceTokens = Object.keys(devicesMap);
76
+ let deviceTokens = Object.keys(devicesMap);
97
77
 
98
- var resolvers = [];
99
- var promises = deviceTokens.map(function () {
100
- return new Promise(function (resolve) {
101
- return resolvers.push(resolve);
102
- });
103
- });
104
- var registrationTokens = deviceTokens;
105
- var length = registrationTokens.length;
106
- _npmlog2.default.verbose(LOG_PREFIX, 'sending to ' + length + ' ' + (length > 1 ? 'devices' : 'device'));
107
- this.sender.send(message, { registrationTokens: registrationTokens }, 5, function (error, response) {
78
+ const resolvers = [];
79
+ const promises = deviceTokens.map(() => new Promise(resolve => resolvers.push(resolve)));
80
+ let registrationTokens = deviceTokens;
81
+ let length = registrationTokens.length;
82
+ log.verbose(LOG_PREFIX, `sending to ${length} ${length > 1 ? 'devices' : 'device'}`);
83
+ this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => {
108
84
  // example response:
109
85
  /*
110
86
  { "multicast_id":7680139367771848000,
@@ -117,24 +93,20 @@ GCM.prototype.send = function (data, devices) {
117
93
  {"error":"InvalidRegistration"}] }
118
94
  */
119
95
  if (error) {
120
- _npmlog2.default.error(LOG_PREFIX, 'send errored: %s', JSON.stringify(error, null, 4));
96
+ log.error(LOG_PREFIX, `send errored: %s`, JSON.stringify(error, null, 4));
121
97
  } else {
122
- _npmlog2.default.verbose(LOG_PREFIX, 'GCM Response: %s', JSON.stringify(response, null, 4));
98
+ log.verbose(LOG_PREFIX, `GCM Response: %s`, JSON.stringify(response, null, 4));
123
99
  }
124
-
125
- var _ref = response || {},
126
- results = _ref.results,
127
- multicast_id = _ref.multicast_id;
128
-
129
- registrationTokens.forEach(function (token, index) {
130
- var resolve = resolvers[index];
131
- var result = results ? results[index] : undefined;
132
- var device = devicesMap[token];
100
+ let { results, multicast_id } = response || {};
101
+ registrationTokens.forEach((token, index) => {
102
+ let resolve = resolvers[index];
103
+ let result = results ? results[index] : undefined;
104
+ let device = devicesMap[token];
133
105
  device.deviceType = 'android';
134
- var resolution = {
135
- device: device,
136
- multicast_id: multicast_id,
137
- response: error || result
106
+ let resolution = {
107
+ device,
108
+ multicast_id,
109
+ response: error || result,
138
110
  };
139
111
  if (!result || result.error) {
140
112
  resolution.transmitted = false;
@@ -145,7 +117,7 @@ GCM.prototype.send = function (data, devices) {
145
117
  });
146
118
  });
147
119
  return Promise.all(promises);
148
- };
120
+ }
149
121
 
150
122
  /**
151
123
  * Generate the gcm payload from the data we get from api request.
@@ -156,24 +128,24 @@ GCM.prototype.send = function (data, devices) {
156
128
  * @returns {Object} A promise which is resolved after we get results from gcm
157
129
  */
158
130
  function generateGCMPayload(requestData, pushId, timeStamp, expirationTime) {
159
- var payload = {
131
+ let payload = {
160
132
  priority: 'high'
161
133
  };
162
134
  payload.data = {
163
135
  data: requestData.data,
164
136
  push_id: pushId,
165
137
  time: new Date(timeStamp).toISOString()
166
- };
167
- var optionalKeys = ['contentAvailable', 'notification'];
168
- optionalKeys.forEach(function (key) {
138
+ }
139
+ const optionalKeys = ['contentAvailable', 'notification'];
140
+ optionalKeys.forEach((key) => {
169
141
  if (requestData.hasOwnProperty(key)) {
170
142
  payload[key] = requestData[key];
171
143
  }
172
144
  });
173
145
 
174
146
  if (expirationTime) {
175
- // The timeStamp and expiration is in milliseconds but gcm requires second
176
- var timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
147
+ // The timeStamp and expiration is in milliseconds but gcm requires second
148
+ let timeToLive = Math.floor((expirationTime - timeStamp) / 1000);
177
149
  if (timeToLive < 0) {
178
150
  timeToLive = 0;
179
151
  }
@@ -192,7 +164,7 @@ function generateGCMPayload(requestData, pushId, timeStamp, expirationTime) {
192
164
  * @returns {Array} An array which contaisn several arries of devices with fixed chunk size
193
165
  */
194
166
  function sliceDevices(devices, chunkSize) {
195
- var chunkDevices = [];
167
+ let chunkDevices = [];
196
168
  while (devices.length > 0) {
197
169
  chunkDevices.push(devices.splice(0, chunkSize));
198
170
  }
@@ -204,4 +176,4 @@ GCM.generateGCMPayload = generateGCMPayload;
204
176
  /* istanbul ignore else */
205
177
  if (process.env.TESTING) {
206
178
  GCM.sliceDevices = sliceDevices;
207
- }
179
+ }
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+ import Parse from 'parse';
3
+ import log from 'npmlog';
4
+ import APNS from './APNS.js';
5
+ import GCM from './GCM.js';
6
+ import FCM from './FCM.js';
7
+ import WEB from './WEB.js';
8
+ import EXPO from './EXPO.js';
9
+ import { classifyInstallations } from './PushAdapterUtils.js';
10
+
11
+ const LOG_PREFIX = 'parse-server-push-adapter';
12
+
13
+ export default class ParsePushAdapter {
14
+
15
+ supportsPushTracking = true;
16
+
17
+ constructor(pushConfig = {}) {
18
+ this.validPushTypes = ['ios', 'osx', 'tvos', 'android', 'fcm', 'web', 'expo'];
19
+ this.senderMap = {};
20
+ // used in PushController for Dashboard Features
21
+ this.feature = {
22
+ immediatePush: true
23
+ };
24
+ let pushTypes = Object.keys(pushConfig);
25
+
26
+ for (let pushType of pushTypes) {
27
+ // adapter may be passed as part of the parse-server initialization
28
+ if (this.validPushTypes.indexOf(pushType) < 0 && pushType != 'adapter') {
29
+ throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
30
+ 'Push to ' + pushType + ' is not supported');
31
+ }
32
+ switch (pushType) {
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
+ }
57
+ }
58
+ }
59
+
60
+ getValidPushTypes() {
61
+ return this.validPushTypes;
62
+ }
63
+
64
+ static classifyInstallations(installations, validTypes) {
65
+ return classifyInstallations(installations, validTypes)
66
+ }
67
+
68
+ send(data, installations) {
69
+ let deviceMap = classifyInstallations(installations, this.validPushTypes);
70
+ let sendPromises = [];
71
+ for (let pushType in deviceMap) {
72
+ let sender = this.senderMap[pushType];
73
+ let devices = deviceMap[pushType];
74
+
75
+ if(Array.isArray(devices) && devices.length > 0) {
76
+ if (!sender) {
77
+ log.verbose(LOG_PREFIX, `Can not find sender for push type ${pushType}, ${data}`)
78
+ let results = devices.map((device) => {
79
+ return Promise.resolve({
80
+ device,
81
+ transmitted: false,
82
+ response: {'error': `Can not find sender for push type ${pushType}, ${data}`}
83
+ })
84
+ });
85
+ sendPromises.push(Promise.all(results));
86
+ } else {
87
+ sendPromises.push(sender.send(data, devices));
88
+ }
89
+ }
90
+ }
91
+ return Promise.all(sendPromises).then((promises) => {
92
+ // flatten all
93
+ return [].concat.apply([], promises);
94
+ })
95
+ }
96
+ }
@@ -0,0 +1,45 @@
1
+ import { randomBytes } from 'crypto';
2
+
3
+ /**g
4
+ * Classify the device token of installations based on its device type.
5
+ * @param {Object} installations An array of installations
6
+ * @param {Array} validPushTypes An array of valid push types(string)
7
+ * @returns {Object} A map whose key is device type and value is an array of device
8
+ */
9
+ export function classifyInstallations(installations, validPushTypes) {
10
+ // Init deviceTokenMap, create a empty array for each valid pushType
11
+ let deviceMap = {};
12
+ for (let validPushType of validPushTypes) {
13
+ deviceMap[validPushType] = [];
14
+ }
15
+ for (let installation of installations) {
16
+ // No deviceToken, ignore
17
+ if (!installation.deviceToken) {
18
+ continue;
19
+ }
20
+ let devices = deviceMap[installation.pushType] || deviceMap[installation.deviceType] || null;
21
+ if (Array.isArray(devices)) {
22
+ devices.push({
23
+ deviceToken: installation.deviceToken,
24
+ deviceType: installation.deviceType,
25
+ appIdentifier: installation.appIdentifier
26
+ });
27
+ }
28
+ }
29
+ return deviceMap;
30
+ }
31
+
32
+ export function randomString(size) {
33
+ if (size === 0) {
34
+ throw new Error('Zero-length randomString is useless.');
35
+ }
36
+ let chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
37
+ 'abcdefghijklmnopqrstuvwxyz' +
38
+ '0123456789');
39
+ let objectId = '';
40
+ let bytes = randomBytes(size);
41
+ for (let i = 0; i < bytes.length; ++i) {
42
+ objectId += chars[bytes.readUInt8(i) % chars.length];
43
+ }
44
+ return objectId;
45
+ }
package/src/WEB.js ADDED
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+
3
+ import Parse from 'parse';
4
+ import log from 'npmlog';
5
+ import webpush from 'web-push';
6
+
7
+ const LOG_PREFIX = 'parse-server-push-adapter WEB';
8
+
9
+ export class WEB {
10
+ /**
11
+ * Create a new WEB push adapter.
12
+ *
13
+ * @param {Object} args https://github.com/web-push-libs/web-push#api-reference
14
+ */
15
+ constructor(args) {
16
+ if (typeof args !== 'object' || !args.vapidDetails) {
17
+ throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'WEB Push Configuration is invalid');
18
+ }
19
+ this.options = args;
20
+ }
21
+
22
+ /**
23
+ * Send web push notification request.
24
+ *
25
+ * @param {Object} data The data we need to send, the format is the same with api request body
26
+ * @param {Array} devices An array of devices
27
+ * @returns {Object} A promise which is resolved immediately
28
+ */
29
+ async send(data, devices) {
30
+ const coreData = data && data.data;
31
+ if (!coreData || !devices || !Array.isArray(devices)) {
32
+ log.warn(LOG_PREFIX, 'invalid push payload');
33
+ return;
34
+ }
35
+ const devicesMap = devices.reduce((memo, device) => {
36
+ memo[device.deviceToken] = device;
37
+ return memo;
38
+ }, {});
39
+ const deviceTokens = Object.keys(devicesMap);
40
+
41
+ const resolvers = [];
42
+ const promises = deviceTokens.map(() => new Promise(resolve => resolvers.push(resolve)));
43
+ let length = deviceTokens.length;
44
+ log.verbose(LOG_PREFIX, `sending to ${length} ${length > 1 ? 'devices' : 'device'}`);
45
+
46
+ const response = await WEB.sendNotifications(coreData, deviceTokens, this.options);
47
+ const { results, sent, failed } = response;
48
+ if (sent) {
49
+ log.verbose(LOG_PREFIX, `WEB Response: %d out of %d sent successfully`, sent, results.length);
50
+ }
51
+ if (failed) {
52
+ log.error(LOG_PREFIX, `send errored: %d out of %d failed with error %s`, failed, results.length, 'push subscription has unsubscribed or expired.');
53
+ }
54
+ deviceTokens.forEach((token, index) => {
55
+ const resolve = resolvers[index];
56
+ const { result, error } = results[index];
57
+ const device = devicesMap[token];
58
+ device.deviceType = 'web';
59
+ const resolution = {
60
+ device,
61
+ response: error || result,
62
+ transmitted: !error,
63
+ };
64
+ resolve(resolution);
65
+ });
66
+ return Promise.all(promises);
67
+ }
68
+
69
+ /**
70
+ * Send multiple web push notification request.
71
+ *
72
+ * @param {Object} payload The data we need to send, the format is the same with api request body
73
+ * @param {Array} deviceTokens An array of devicesTokens
74
+ * @param {Object} options The options for the request
75
+ * @returns {Object} A promise which is resolved immediately
76
+ */
77
+ static async sendNotifications(payload, deviceTokens, options) {
78
+ const promises = deviceTokens.map((deviceToken) => {
79
+ if (typeof deviceToken === 'string') {
80
+ deviceToken = JSON.parse(deviceToken);
81
+ }
82
+ if (typeof payload === 'object') {
83
+ payload = JSON.stringify(payload);
84
+ }
85
+ return webpush.sendNotification(deviceToken, payload, options);
86
+ });
87
+ const allResults = await Promise.allSettled(promises);
88
+ const response = {
89
+ sent: 0,
90
+ failed: 0,
91
+ results: [],
92
+ };
93
+ allResults.forEach((result) => {
94
+ if (result.status === 'fulfilled') {
95
+ response.sent += 1;
96
+ response.results.push({ result: result.value.statusCode });
97
+ } else {
98
+ response.failed += 1;
99
+ response.results.push({ error: result.reason.body });
100
+ }
101
+ });
102
+ return response;
103
+ }
104
+ }
105
+
106
+ export default WEB;
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ // ParsePushAdapter is the default implementation of
3
+ // PushAdapter, it uses GCM for android push, APNS for ios push.
4
+ // WEB for web push.
5
+ import log from 'npmlog';
6
+
7
+ /* istanbul ignore if */
8
+ if (process.env.VERBOSE || process.env.VERBOSE_PARSE_SERVER_PUSH_ADAPTER) {
9
+ log.level = 'verbose';
10
+ }
11
+
12
+ import ParsePushAdapter from './ParsePushAdapter.js';
13
+ import GCM from './GCM.js';
14
+ import APNS from './APNS.js';
15
+ import WEB from './WEB.js';
16
+ import EXPO from './EXPO.js';
17
+ import * as utils from './PushAdapterUtils.js';
18
+
19
+ export default ParsePushAdapter;
20
+ export { ParsePushAdapter, APNS, GCM, WEB, EXPO, utils };