diff --git a/plugins/push/api/send/platforms/a.js b/plugins/push/api/send/platforms/a.js index 94418d33063..0f85ca42132 100644 --- a/plugins/push/api/send/platforms/a.js +++ b/plugins/push/api/send/platforms/a.js @@ -1,4 +1,4 @@ -const { ConnectionError, ERROR, SendError, PushError, FCM_SDK_ERRORS } = require('../data/error'), +const { ERROR, SendError, FCM_SDK_ERRORS } = require('../data/error'), logger = require('../../../../../api/utils/log'), { Splitter } = require('./utils/splitter'), { util } = require('../std'), @@ -56,7 +56,7 @@ class FCM extends Splitter { * Standard constructor * @param {string} log logger name * @param {string} type type of connection: ap, at, id, ia, ip, ht, hp - * @param {Credentials} creds FCM server key + * @param {Credentials} creds FCM credentials * @param {Object[]} messages initial array of messages to send * @param {Object} options standard stream options * @param {number} options.pool.pushes number of notifications which can be processed concurrently, this parameter is strictly set to 500 @@ -71,34 +71,20 @@ class FCM extends Splitter { this.legacyApi = !creds._data.serviceAccountFile; this.log = logger(log).sub(`${threadId}-a`); - if (this.legacyApi) { - this.opts = { - agent: this.agent, - hostname: 'fcm.googleapis.com', - port: 443, - path: '/fcm/send', - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `key=${creds._data.key}`, - }, - }; - } - else { - const serviceAccountJSON = FORGE.util.decode64( - creds._data.serviceAccountFile.substring(creds._data.serviceAccountFile.indexOf(',') + 1) - ); - const serviceAccountObject = JSON.parse(serviceAccountJSON); - const appName = creds._data.hash; // using hash as the app name - const firebaseApp = firebaseAdmin.apps.find(app => app.name === appName) - ? firebaseAdmin.app(appName) - : firebaseAdmin.initializeApp({ - credential: firebaseAdmin.credential.cert(serviceAccountObject, this.agent), - httpAgent: this.agent - }, appName); - this.firebaseMessaging = firebaseApp.messaging(); - } + + const serviceAccountJSON = FORGE.util.decode64( + creds._data.serviceAccountFile.substring(creds._data.serviceAccountFile.indexOf(',') + 1) + ); + const serviceAccountObject = JSON.parse(serviceAccountJSON); + const appName = creds._data.hash; // using hash as the app name + const firebaseApp = firebaseAdmin.apps.find(app => app.name === appName) + ? firebaseAdmin.app(appName) + : firebaseAdmin.initializeApp({ + credential: firebaseAdmin.credential.cert(serviceAccountObject, this.agent), + httpAgent: this.agent + }, appName); + this.firebaseMessaging = firebaseApp.messaging(); + this.log.i('Initialized'); } @@ -139,8 +125,6 @@ class FCM extends Splitter { const one = Math.ceil(bytes / pushes.length); let content = this.template(pushes[0].m).compile(pushes[0]); - let printBody = false; - const oks = []; const errors = {}; /** * Get an error for given code & message, create it if it doesn't exist yet @@ -156,221 +140,66 @@ class FCM extends Splitter { } return errors[err]; }; - if (!this.legacyApi) { - const tokens = pushes.map(p => p.t); - - // new fcm api doesn't allow objects or arrays inside "data" property - if (content.data && typeof content.data === "object") { - for (let prop in content.data) { - if (content.data[prop] && typeof content.data[prop] === "object") { - content.data[prop] = JSON.stringify(content.data[prop]); - } - } - } - const messages = tokens.map(token => ({ - token, - ...content, - })); - - return this.firebaseMessaging - // EXAMPLE RESPONSE of sendEach - // { - // "responses": [ - // { - // "success": false, - // "error": { - // "code": "messaging/invalid-argument", - // "message": "The registration token is not a valid FCM registration token" - // } - // } - // ], - // "successCount": 0, - // "failureCount": 1 - // } - .sendEach(messages) - .then(async result => { - const allPushIds = pushes.map(p => p._id); - - if (!result.failureCount) { - this.send_results(allPushIds, bytes); - return; - } + const tokens = pushes.map(p => p.t); + const messages = tokens.map(token => ({ + token, + ...content, + })); - // array of successfully sent push._id: - const sentSuccessfully = []; - - // check for each message - for (let i = 0; i < result.responses.length; i++) { - const { success, error } = result.responses[i]; - if (success) { - sentSuccessfully.push(allPushIds[i]); - } - else { - const sdkError = FCM_SDK_ERRORS[error.code]; - // check if the sdk error is mapped to an internal error. - // set to default if its not. - let internalErrorCode = sdkError?.mapTo ?? ERROR.DATA_PROVIDER; - let internalErrorMessage = sdkError?.message ?? "Invalid error message"; - errorObject(internalErrorCode, internalErrorMessage) - .addAffected(pushes[i]._id, one); - } - } - // send results back: - for (let errorKey in errors) { - this.send_push_error(errors[errorKey]); - } - if (sentSuccessfully.length) { - this.send_results(sentSuccessfully, one * sentSuccessfully.length); - } - }); - } - - content.registration_ids = pushes.map(p => p.t); - - // CONNECTION TEST PAYLOAD (invalid registration token) + return this.firebaseMessaging + // EXAMPLE RESPONSE of sendEach // { - // "data": { - // "c.i": "663389aab53ebbf71a115edb", - // "message": "test" - // }, - // "registration_ids": [ - // "0.2124088209996502" - // ] + // "responses": [ + // { + // "success": false, + // "error": { + // "code": "messaging/invalid-argument", + // "message": "The registration token is not a valid FCM registration token" + // } + // } + // ], + // "successCount": 0, + // "failureCount": 1 // } - // NORMAL PAYLOAD - // { - // "data": { - // "c.i": "663389a949c58657a8e625b3", - // "title": "qwer", - // "message": "qwer", - // "sound": "default" - // }, - // "registration_ids": [ - // "dw_CueiXThqYI9owrQC0Pb:APA91bHanJn9RM-ZYnC-3wCMld5Nk3QaVJppS4HOKrpdV8kCXq7pjQlJjcd8_1xq9G6XaceZfrFPxbfehJ4YCEfMsfQVhZW1WKhnY3TbtO7HIQfYfbj35-sx_-BHAhQ5eSDuiCOZWUDP" - // ] - // } - return this.sendRequest(JSON.stringify(content)).then(resp => { - // CONNECTION TEST RESPONSE (with error) - // { - // "multicast_id": 2829871343601014000, - // "success": 0, - // "failure": 1, - // "canonical_ids": 0, - // "results": [ - // { - // "error": "InvalidRegistration" - // } - // ] - // } - // NORMAL SUCCESSFUL RESPONSE - // { - // "multicast_id": 5676989510572196000, - // "success": 1, - // "failure": 0, - // "canonical_ids": 0, - // "results": [ - // { - // "message_id": "0:1714653611139550%68dc6e82f9fd7ecd" - // } - // ] - // } - try { - resp = JSON.parse(resp); - } - catch (error) { - this.log.e('Bad FCM response format: %j', resp, error); - throw PushError.deserialize(error, SendError); - } + .sendEach(messages) + .then(async result => { + const allPushIds = pushes.map(p => p._id); - if (resp.failure === 0 && resp.canonical_ids === 0) { - this.send_results(pushes.map(p => p._id), bytes); - return; - } + if (!result.failureCount) { + this.send_results(allPushIds, bytes); + return; + } - if (resp.results) { - resp.results.forEach((r, i) => { - if (r.message_id) { - if (r.registration_id) { - if (r.registration_id === 'BLACKLISTED') { - errorObject(ERROR.DATA_TOKEN_INVALID, 'Blacklisted').addAffected(pushes[i]._id, one); - printBody = true; - } - else { - oks.push([pushes[i]._id, r.registration_id]); - } - // oks.push([pushes[i]._id, r.registration_id], one); ??? - } - else { - oks.push(pushes[i]._id); - } - } - else if (r.error === 'NotRegistered') { - this.log.d('Token %s expired (%s)', pushes[i].t, r.error); - errorObject(ERROR.DATA_TOKEN_EXPIRED, r.error).addAffected(pushes[i]._id, one); - } - else if (r.error === 'InvalidRegistration' || r.error === 'MismatchSenderId' || r.error === 'InvalidPackageName') { - this.log.d('Token %s is invalid (%s)', pushes[i].t, r.error); - errorObject(ERROR.DATA_TOKEN_INVALID, r.error).addAffected(pushes[i]._id, one); + // array of successfully sent push._id: + const sentSuccessfully = []; + + // check for each message + for (let i = 0; i < result.responses.length; i++) { + const { success, error } = result.responses[i]; + if (success) { + sentSuccessfully.push(allPushIds[i]); } - // these are identical to "else" block: - // else if (r.error === 'InvalidParameters') { // still hasn't figured out why this error is thrown, therefore not critical yet - // printBody = true; - // errorObject(ERROR.DATA_PROVIDER, r.error).addAffected(pushes[i]._id, one); - // } - // else if (r.error === 'MessageTooBig' || r.error === 'InvalidDataKey' || r.error === 'InvalidTtl') { - // printBody = true; - // errorObject(ERROR.DATA_PROVIDER, r.error).addAffected(pushes[i]._id, one); - // } else { - printBody = true; - errorObject(ERROR.DATA_PROVIDER, r.error).addAffected(pushes[i]._id, one); + const sdkError = FCM_SDK_ERRORS[error.code]; + // check if the sdk error is mapped to an internal error. + // set to default if its not. + let internalErrorCode = sdkError?.mapTo ?? ERROR.DATA_PROVIDER; + let internalErrorMessage = sdkError?.message ?? "Invalid error message"; + errorObject(internalErrorCode, internalErrorMessage) + .addAffected(pushes[i]._id, one); } - }); - let errored = 0; - for (let k in errors) { - errored += errors[k].affectedBytes; - this.send_push_error(errors[k]); } - if (oks.length) { - this.send_results(oks, bytes - errored); + // send results back: + for (let errorKey in errors) { + this.send_push_error(errors[errorKey]); } - if (printBody) { - this.log.e('Provider returned error %j for %j', resp, content); - } - } - }, ([code, error]) => { - this.log.w('FCM error %d / %j', code, error); - console.log("========== MAIN PROMISE ERROR"); - if (code === 0) { - if (error.message === 'ECONNRESET' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT' || - error.code === 'ECONNREFUSED' || error.code === 'ECONNABORTED' || error.code === 'EHOSTUNREACH' || - error.code === 'EAI_AGAIN') { - this.log.w('FCM error %d / %j', bytes, pushes.map(p => p._id)); - throw new ConnectionError(`FCM ${error.code}`, ERROR.CONNECTION_PROVIDER) - .setConnectionError(error.code, `${error.errno} ${error.code} ${error.syscall}`) - .addAffected(pushes.map(p => p._id), bytes); + if (sentSuccessfully.length) { + this.send_results(sentSuccessfully, one * sentSuccessfully.length); } - let pe = PushError.deserialize(error, SendError); - pe.addAffected(pushes.map(p => p._id), bytes); - throw pe; - } - else if (code >= 500) { - throw new ConnectionError(`FCM Unavailable: ${code}`, ERROR.CONNECTION_PROVIDER).addAffected(pushes.map(p => p._id), bytes); - } - else if (code === 401) { - throw new ConnectionError(`FCM Unauthorized: ${code}`, ERROR.INVALID_CREDENTIALS).addAffected(pushes.map(p => p._id), bytes); - } - else if (code === 400) { - throw new ConnectionError(`FCM Bad message: ${code}`, ERROR.DATA_PROVIDER).addAffected(pushes.map(p => p._id), bytes); - } - else { - throw new ConnectionError(`FCM Bad response code: ${code}`, ERROR.EXCEPTION).addAffected(pushes.map(p => p._id), bytes); - } - }); + }); }); } - } /** @@ -581,7 +410,6 @@ const CREDS = { static get scheme() { return Object.assign(super.scheme, { serviceAccountFile: { required: false, type: "String" }, - key: { required: false, type: 'String', 'min-length': 100}, hash: { required: false, type: 'String' }, }); } @@ -621,9 +449,6 @@ const CREDS = { } this._data.hash = FORGE.md.sha256.create().update(serviceAccountJSON).digest().toHex(); } - else if (this._data.key) { - this._data.hash = FORGE.md.sha256.create().update(this._data.key).digest().toHex(); - } else { return ["Updating FCM credentials requires a service-account.json file"]; } @@ -635,16 +460,12 @@ const CREDS = { * @returns {object} json without sensitive information */ get view() { - const fcmKey = this._data?.key - ? `FCM server key "${this._data.key.substr(0, 10)} ... ${this._data.key.substr(this._data.key.length - 10)}"` - : ""; const serviceAccountFile = this._data?.serviceAccountFile ? "service-account.json" : ""; return { _id: this._id, type: this._data?.type, - key: fcmKey, serviceAccountFile, hash: this._data?.hash, }; diff --git a/plugins/push/frontend/public/javascripts/countly.models.js b/plugins/push/frontend/public/javascripts/countly.models.js index 00277f01d0f..d4334958814 100644 --- a/plugins/push/frontend/public/javascripts/countly.models.js +++ b/plugins/push/frontend/public/javascripts/countly.models.js @@ -737,16 +737,6 @@ data: data, dataType: "json", success: function(response) { - const notificationId = "legacy-fcm-warning"; - CountlyHelpers.removePersistentNotification(notificationId); - if (typeof response?.legacyFcm === "boolean" && response.legacyFcm) { - CountlyHelpers.notify({ - id: notificationId, - message: CV.i18n("push-notification.legacy-fcm-warning"), - type: "error", - persistent: true - }); - } resolve(response); }, error: function(error) { @@ -1774,10 +1764,8 @@ }, mapAndroidAppLevelConfig: function(dto) { if (this.hasAppLevelPlatformConfig(dto, PlatformDtoEnum.ANDROID)) { - console.log(dto[PlatformDtoEnum.ANDROID]); return { _id: dto[PlatformDtoEnum.ANDROID]._id || '', - firebaseKey: dto[PlatformDtoEnum.ANDROID].key, serviceAccountFile: dto[PlatformDtoEnum.ANDROID].serviceAccountFile, type: dto[PlatformDtoEnum.ANDROID].type, hasServiceAccountFile: !!dto[PlatformDtoEnum.ANDROID].serviceAccountFile, @@ -2357,9 +2345,6 @@ if (model[PlatformEnum.ANDROID].hasUploadedServiceAccountFile) { result.serviceAccountFile = model[PlatformEnum.ANDROID].serviceAccountFile; } - else { - result.key = model[PlatformEnum.ANDROID].firebaseKey; - } if (model[PlatformEnum.ANDROID]._id) { result._id = model[PlatformEnum.ANDROID]._id; diff --git a/plugins/push/frontend/public/javascripts/countly.views.js b/plugins/push/frontend/public/javascripts/countly.views.js index f5daf0546df..dbd5b16f74b 100644 --- a/plugins/push/frontend/public/javascripts/countly.views.js +++ b/plugins/push/frontend/public/javascripts/countly.views.js @@ -2267,7 +2267,6 @@ }; initialAppLevelConfig[countlyPushNotification.service.PlatformEnum.ANDROID] = { _id: "", - firebaseKey: "", serviceAccountFile: "", type: "fcm", hasServiceAccountFile: false, @@ -2460,7 +2459,7 @@ }, isKeyEmpty: function(platform) { if (platform === this.PlatformEnum.ANDROID) { - return !this.viewModel[platform].firebaseKey && !this.viewModel[platform].serviceAccountFile; + return !this.viewModel[platform].serviceAccountFile; } if (platform === this.PlatformEnum.IOS) { if (this.iosAuthConfigType === countlyPushNotification.service.IOSAuthConfigTypeEnum.P8) { diff --git a/plugins/push/frontend/public/localization/push.properties b/plugins/push/frontend/public/localization/push.properties index f90748d5840..5f0da5c60bb 100755 --- a/plugins/push/frontend/public/localization/push.properties +++ b/plugins/push/frontend/public/localization/push.properties @@ -277,7 +277,6 @@ push-notification.was-successfully-rejected = Push notification has been success push-notification.was-successfully-deleted = Push notification was successfully deleted push-notification.was-successfully-started = Push notification was successfully started push-notification.was-successfully-stopped = Push notification was successfully stopped -push-notification.legacy-fcm-warning = Your Android Firebase configuration will be deprecated and will stop working on June 20, 2024. Please update the configuration with a service-account.json file. # Add user property push-notification.event-properties = Event Properties @@ -374,7 +373,6 @@ push-notification.team-id = Team ID push-notification.bundle-id = Bundle ID push-notification.passphrase = Passphrase push-notification.android-settings = Android (Google FCM) -push-notification.firebase-key-legacy = Legacy Firebase key (will be deprecated on June 20th 2024) push-notification.firebase-service-account-json = Service account JSON file push-notification.service-account-file-already-uploaded = Service account JSON file is already uploaded push-notification.huawei-settings = Android (Huawei Push Kit) diff --git a/plugins/push/frontend/public/templates/push-notification-app-config.html b/plugins/push/frontend/public/templates/push-notification-app-config.html index 93b44439ea1..18641bad319 100644 --- a/plugins/push/frontend/public/templates/push-notification-app-config.html +++ b/plugins/push/frontend/public/templates/push-notification-app-config.html @@ -52,9 +52,6 @@

{{i18n('push-notification.ios-settings')}}

{{i18n('push-notification.android-settings')}}

{{i18n('push-notification.delete')}} - - -