123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- 'use strict';
- import apn from '@parse/node-apn';
- import Parse from 'parse';
- import log from 'npmlog';
- const LOG_PREFIX = 'parse-server-push-adapter APNS';
- export class APNS {
- /**
- * Create a new provider for the APN service.
- * @constructor
- * @param {Object|Array} args An argument or a list of arguments to config APNS provider
- * @param {Object} args.token {Object} Configuration for Provider Authentication Tokens. (Defaults to: null i.e. fallback to Certificates)
- * @param {Buffer|String} args.token.key The filename of the provider token key (as supplied by Apple) to load from disk, or a Buffer/String containing the key data.
- * @param {String} args.token.keyId The ID of the key issued by Apple
- * @param {String} args.token.teamId ID of the team associated with the provider token key
- * @param {Buffer|String} args.cert The filename of the connection certificate to load from disk, or a Buffer/String containing the certificate data.
- * @param {Buffer|String} args.key {Buffer|String} The filename of the connection key to load from disk, or a Buffer/String containing the key data.
- * @param {Buffer|String} args.pfx path for private key, certificate and CA certs in PFX or PKCS12 format, or a Buffer containing the PFX data. If supplied will always be used instead of certificate and key above.
- * @param {String} args.passphrase The passphrase for the provider key, if required
- * @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox
- * @param {String} args.topic Specififies an App-Id for this Provider
- * @param {String} args.bundleId DEPRECATED: Specifies an App-ID for this Provider
- * @param {Number} args.connectionRetryLimit The maximum number of connection failures that will be tolerated before apn.Provider will "give up". (Defaults to: 3)
- */
- constructor(args) {
- // Define class members
- this.providers = [];
- // Since for ios, there maybe multiple cert/key pairs, typePushConfig can be an array.
- let apnsArgsList = [];
- if (Array.isArray(args)) {
- apnsArgsList = apnsArgsList.concat(args);
- } else if (typeof args === 'object') {
- apnsArgsList.push(args);
- } else {
- throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'APNS Configuration is invalid');
- }
- // Create Provider from each arg-object
- for (const apnsArgs of apnsArgsList) {
- // rewrite bundleId to topic for backward-compatibility
- if (apnsArgs.bundleId) {
- log.warn(LOG_PREFIX, 'bundleId is deprecated, use topic instead');
- apnsArgs.topic = apnsArgs.bundleId
- }
- const provider = APNS._createProvider(apnsArgs);
- this.providers.push(provider);
- }
- // Sort the providers based on priority ascending, high pri first
- this.providers.sort((s1, s2) => {
- return s1.priority - s2.priority;
- });
- // Set index-property of providers
- for (let index = 0; index < this.providers.length; index++) {
- this.providers[index].index = index;
- }
- }
- /**
- * Send apns request.
- *
- * @param {Object} data The data we need to send, the format is the same with api request body
- * @param {Array} allDevices An array of devices
- * @returns {Object} A promise which is resolved immediately
- */
- send(data, allDevices) {
- const coreData = data && data.data;
- if (!coreData || !allDevices || !Array.isArray(allDevices)) {
- log.warn(LOG_PREFIX, 'invalid push payload');
- return;
- }
- const expirationTime = data['expiration_time'] || coreData['expiration_time'];
- const collapseId = data['collapse_id'] || coreData['collapse_id'];
- const pushType = data['push_type'] || coreData['push_type'];
- const priority = data['priority'] || coreData['priority'];
- let allPromises = [];
- const devicesPerAppIdentifier = {};
- // Start by clustering the devices per appIdentifier
- allDevices.forEach(device => {
- const appIdentifier = device.appIdentifier;
- devicesPerAppIdentifier[appIdentifier] = devicesPerAppIdentifier[appIdentifier] || [];
- devicesPerAppIdentifier[appIdentifier].push(device);
- });
- for (const key in devicesPerAppIdentifier) {
- const devices = devicesPerAppIdentifier[key];
- const appIdentifier = devices[0].appIdentifier;
- const providers = this._chooseProviders(appIdentifier);
- // No Providers found
- if (!providers || providers.length === 0) {
- const errorPromises = devices.map(device => APNS._createErrorPromise(device.deviceToken, 'No Provider found'));
- allPromises = allPromises.concat(errorPromises);
- continue;
- }
- const headers = { expirationTime: expirationTime, topic: appIdentifier, collapseId: collapseId, pushType: pushType, priority: priority }
- const notification = APNS._generateNotification(coreData, headers);
- const deviceIds = devices.map(device => device.deviceToken);
- const promise = this.sendThroughProvider(notification, deviceIds, providers);
- allPromises.push(promise.then(this._handlePromise.bind(this)));
- }
- return Promise.all(allPromises).then((results) => {
- // flatten all
- return [].concat.apply([], results);
- });
- }
- sendThroughProvider(notification, devices, providers) {
- return providers[0]
- .send(notification, devices)
- .then((response) => {
- if (response.failed
- && response.failed.length > 0
- && providers && providers.length > 1) {
- const devices = response.failed.map((failure) => { return failure.device; });
- // Reset the failures as we'll try next connection
- response.failed = [];
- return this.sendThroughProvider(notification,
- devices,
- providers.slice(1, providers.length)).then((retryResponse) => {
- response.failed = response.failed.concat(retryResponse.failed);
- response.sent = response.sent.concat(retryResponse.sent);
- return response;
- });
- } else {
- return response;
- }
- });
- }
- static _validateAPNArgs(apnsArgs) {
- if (apnsArgs.topic) {
- return true;
- }
- return !(apnsArgs.cert || apnsArgs.key || apnsArgs.pfx);
- }
- /**
- * Creates an Provider base on apnsArgs.
- */
- static _createProvider(apnsArgs) {
- // if using certificate, then topic must be defined
- if (!APNS._validateAPNArgs(apnsArgs)) {
- throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is mssing for %j', apnsArgs);
- }
- const provider = new apn.Provider(apnsArgs);
- // Sets the topic on this provider
- provider.topic = apnsArgs.topic;
- // Set the priority of the providers, prod cert has higher priority
- if (apnsArgs.production) {
- provider.priority = 0;
- } else {
- provider.priority = 1;
- }
- return provider;
- }
- /**
- * Generate the apns Notification from the data we get from api request.
- * @param {Object} coreData The data field under api request body
- * @param {Object} headers The header properties for the notification (topic, expirationTime, collapseId, pushType, priority)
- * @returns {Object} A apns Notification
- */
- static _generateNotification(coreData, headers) {
- const notification = new apn.Notification();
- const payload = {};
- for (const key in coreData) {
- switch (key) {
- case 'aps':
- notification.aps = coreData.aps;
- break;
- case 'alert':
- notification.setAlert(coreData.alert);
- break;
- case 'title':
- notification.setTitle(coreData.title);
- break;
- case 'badge':
- notification.setBadge(coreData.badge);
- break;
- case 'sound':
- notification.setSound(coreData.sound);
- break;
- case 'content-available':
- notification.setContentAvailable(coreData['content-available'] === 1);
- break;
- case 'mutable-content':
- notification.setMutableContent(coreData['mutable-content'] === 1);
- break;
- case 'targetContentIdentifier':
- notification.setTargetContentIdentifier(coreData.targetContentIdentifier);
- break;
- case 'interruptionLevel':
- notification.setInterruptionLevel(coreData.interruptionLevel);
- break;
- case 'category':
- notification.setCategory(coreData.category);
- break;
- case 'threadId':
- notification.setThreadId(coreData.threadId);
- break;
- default:
- payload[key] = coreData[key];
- break;
- }
- }
- notification.payload = payload;
- notification.topic = headers.topic;
- notification.expiry = Math.round(headers.expirationTime / 1000);
- notification.collapseId = headers.collapseId;
- // set alert as default push type. If push type is not set notifications are not delivered to devices running iOS 13, watchOS 6 and later.
- notification.pushType = 'alert';
- if (headers.pushType) {
- notification.pushType = headers.pushType;
- }
- if (headers.priority) {
- // if headers priority is not set 'node-apn' defaults it to 5 which is min. required value for background pushes to launch the app in background.
- notification.priority = headers.priority
- }
- return notification;
- }
- /**
- * Choose appropriate providers based on device appIdentifier.
- *
- * @param {String} appIdentifier appIdentifier for required provider
- * @returns {Array} Returns Array with appropriate providers
- */
- _chooseProviders(appIdentifier) {
- // If the device we need to send to does not have appIdentifier, any provider could be a qualified provider
- /*if (!appIdentifier || appIdentifier === '') {
- return this.providers.map((provider) => provider.index);
- }*/
- // Otherwise we try to match the appIdentifier with topic on provider
- const qualifiedProviders = this.providers.filter((provider) => appIdentifier === provider.topic);
- if (qualifiedProviders.length > 0) {
- return qualifiedProviders;
- }
- // If qualifiedProviders empty, add all providers without topic
- return this.providers
- .filter((provider) => !provider.topic || provider.topic === '');
- }
- _handlePromise(response) {
- const promises = [];
- response.sent.forEach((token) => {
- log.verbose(LOG_PREFIX, 'APNS transmitted to %s', token.device);
- promises.push(APNS._createSuccesfullPromise(token.device));
- });
- response.failed.forEach((failure) => {
- promises.push(APNS._handlePushFailure(failure));
- });
- return Promise.all(promises);
- }
- static _handlePushFailure(failure) {
- if (failure.error) {
- log.error(LOG_PREFIX, 'APNS error transmitting to device %s with error %s', failure.device, failure.error);
- return APNS._createErrorPromise(failure.device, failure.error);
- } else if (failure.status && failure.response && failure.response.reason) {
- log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason);
- return APNS._createErrorPromise(failure.device, failure.response.reason);
- } else {
- log.error(LOG_PREFIX, 'APNS error transmitting to device with unkown error');
- return APNS._createErrorPromise(failure.device, 'Unkown status');
- }
- }
- /**
- * Creates an errorPromise for return.
- *
- * @param {String} token Device-Token
- * @param {String} errorMessage ErrrorMessage as string
- */
- static _createErrorPromise(token, errorMessage) {
- return Promise.resolve({
- transmitted: false,
- device: {
- deviceToken: token,
- deviceType: 'ios'
- },
- response: { error: errorMessage }
- });
- }
- /**
- * Creates an successfulPromise for return.
- *
- * @param {String} token Device-Token
- */
- static _createSuccesfullPromise(token) {
- return Promise.resolve({
- transmitted: true,
- device: {
- deviceToken: token,
- deviceType: 'ios'
- }
- });
- }
- }
- export default APNS;
|