backend-manager 4.2.8 → 4.2.10

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/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v18/*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "4.2.8",
3
+ "version": "4.2.10",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@firebase/rules-unit-testing": "^2.0.7",
45
- "@google-cloud/storage": "^7.14.0",
45
+ "@google-cloud/storage": "^7.15.0",
46
46
  "@octokit/rest": "^19.0.13",
47
47
  "@sendgrid/mail": "^7.7.0",
48
48
  "@sentry/node": "^6.19.7",
@@ -52,15 +52,15 @@
52
52
  "cors": "^2.8.5",
53
53
  "dotenv": "^16.4.7",
54
54
  "express": "^4.21.2",
55
- "firebase-admin": "^12.7.0",
56
- "firebase-functions": "^6.1.2",
55
+ "firebase-admin": "^13.0.2",
56
+ "firebase-functions": "^6.3.1",
57
57
  "fs-jetpack": "^5.1.0",
58
- "glob": "^11.0.0",
58
+ "glob": "^11.0.1",
59
59
  "hcaptcha": "^0.1.1",
60
60
  "inquirer": "^8.2.5",
61
61
  "itwcw-package-analytics": "^1.0.6",
62
62
  "json5": "^2.2.3",
63
- "jwt-decode": "^3.1.2",
63
+ "jwt-decode": "^4.0.0",
64
64
  "lodash": "^4.17.21",
65
65
  "lowdb": "^1.0.0",
66
66
  "mailchimp-api-v3": "^1.15.0",
@@ -69,20 +69,20 @@
69
69
  "moment": "^2.30.1",
70
70
  "nanoid": "^3.3.8",
71
71
  "node-fetch": "^2.7.0",
72
- "node-powertools": "^1.7.0",
72
+ "node-powertools": "^2.1.2",
73
73
  "npm-api": "^1.0.1",
74
74
  "paypal-server-api": "^2.0.14",
75
75
  "pushid": "^1.0.0",
76
76
  "resolve-account": "^1.0.26",
77
- "shortid": "^2.2.16",
77
+ "shortid": "^2.2.17",
78
78
  "sizeitup": "^1.0.9",
79
79
  "uid-generator": "^2.0.0",
80
80
  "ultimate-jekyll-poster": "^1.0.2",
81
81
  "uuid": "^9.0.1",
82
- "wonderful-fetch": "^1.3.0",
82
+ "wonderful-fetch": "^1.3.1",
83
83
  "wonderful-log": "^1.0.7",
84
84
  "wonderful-version": "^1.2.0",
85
- "yaml": "^2.6.1",
85
+ "yaml": "^2.7.0",
86
86
  "yargs": "^17.7.2"
87
87
  },
88
88
  "devDependencies": {
package/src/cli/cli.js CHANGED
@@ -378,11 +378,18 @@ Main.prototype.setup = async function () {
378
378
  // return isLocal(mine) || !(semver.gt(latest, mine));
379
379
  // }, fix_mocha);
380
380
 
381
+ // Test: Do the dependencies work
382
+ // await self.test(`dependencies work`, function () {
383
+ // return true;
384
+ // }, NOFIX);
385
+
381
386
  // Test: Does the project have a "npm start" script
382
387
  await self.test(`has "npm start" script`, function () {
383
388
  return self.package.scripts.start
384
389
  }, fix_startScript);
385
390
 
391
+
392
+
386
393
  // Test: Does the project have a "npm dist" script
387
394
  await self.test(`has "npm dist" script`, function () {
388
395
  return self.package.scripts.dist
@@ -0,0 +1,265 @@
1
+ // Module
2
+ function Module() {
3
+
4
+ }
5
+
6
+ // Main
7
+ Module.prototype.main = function () {
8
+ const self = this;
9
+
10
+ // Shortcuts
11
+ const Manager = self.Manager;
12
+ const Api = self.Api;
13
+ const assistant = self.assistant;
14
+ const payload = self.payload;
15
+
16
+ return new Promise(async function(resolve, reject) {
17
+ // Load libraries
18
+ const fetch = Manager.require('wonderful-fetch');
19
+
20
+ // Set up response obj
21
+ payload.response.data = {};
22
+
23
+ // Fix notification payload
24
+ const email = payload.data.payload;
25
+
26
+ // Log
27
+ assistant.log('Resolved email payload', email)
28
+
29
+ // Check if user is admin
30
+ if (!payload.user.roles.admin) {
31
+ return reject(assistant.errorify(`Admin required.`, {code: 401}));
32
+ }
33
+
34
+ // Attach BEM key since we are authorized
35
+ email.backendManagerKey = Manager?.config?.backend_manager?.key;
36
+
37
+ // Make request to ITW email service
38
+ await fetch('https://us-central1-itw-creative-works.cloudfunctions.net/sendEmail', {
39
+ method: 'POST',
40
+ response: 'json',
41
+ body: email,
42
+ })
43
+ .then((r) => {
44
+ // Set response
45
+ payload.response.data = r;
46
+
47
+ // Resolve
48
+ return resolve({data: payload.response.data})
49
+ })
50
+ .catch((e) => {
51
+ return reject(assistant.errorify(`Failed to send email: ${e}`, {code: 400, sentry: true}));
52
+ })
53
+ });
54
+
55
+ };
56
+
57
+ // HELPERS //
58
+ Module.prototype.processTokens = async function (note, options) {
59
+ const self = this;
60
+
61
+ // Shortcuts
62
+ const Manager = self.Manager;
63
+ const Api = self.Api;
64
+ const assistant = self.assistant;
65
+ const payload = self.payload;
66
+
67
+ // Set options
68
+ options = options || {};
69
+ options.tags = options.tags || false;
70
+
71
+ // Define collection path
72
+ const queryConditions = options.tags
73
+ ? [{ field: 'tags', operator: 'array-contains-any', value: options.tags }]
74
+ : [];
75
+
76
+ // Batch processing logic
77
+ await Manager.Utilities().iterateCollection(
78
+ async (batch, index) => {
79
+ let batchTokens = [];
80
+
81
+ // Collect tokens from the current batch
82
+ for (const doc of batch.docs) {
83
+ const data = doc.data();
84
+ batchTokens.push(data.token);
85
+ }
86
+
87
+ // Send the batch
88
+ try {
89
+ assistant.log(`Sending batch ${index} with ${batchTokens.length} tokens.`);
90
+ await self.sendBatch(batchTokens, note, index);
91
+ } catch (e) {
92
+ assistant.error(`Error sending batch ${index}`, e);
93
+ }
94
+ },
95
+ {
96
+ collection: PATH_NOTIFICATIONS,
97
+ where: queryConditions,
98
+ batchSize: BATCH_SIZE,
99
+ log: true,
100
+ }
101
+ )
102
+ .then(() => {
103
+ assistant.log('All batches processed successfully.');
104
+ })
105
+ .catch(e => {
106
+ assistant.errorify(`Error during token processing: ${e}`, { code: 500, log: true });
107
+ });
108
+ };
109
+
110
+ // Sending using SDK
111
+ // https://firebase.google.com/docs/cloud-messaging/send-message#send-messages-to-multiple-devices
112
+
113
+ // Manually sending
114
+ // https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send
115
+ // https://firebase.google.com/docs/cloud-messaging/migrate-v1#provide-credentials-manually
116
+ Module.prototype.sendBatch = async function (batch, note, id) {
117
+ const self = this;
118
+
119
+ // Shortcuts
120
+ const Manager = self.Manager;
121
+ const Api = self.Api;
122
+ const assistant = self.assistant;
123
+ const payload = self.payload;
124
+
125
+ // Libraries
126
+ const { admin } = self.libraries;
127
+
128
+ try {
129
+ // Log
130
+ assistant.log(`Sending batch ID: ${id}`);
131
+ console.log('🚩🚩🚩🚩🚩', 1, ); // FLAG
132
+ assistant.log(`batch`, batch);
133
+ assistant.log(`note`, note);
134
+
135
+ try {
136
+ const response = await admin.messaging().sendMulticast({
137
+ tokens: batch,
138
+ notification: {
139
+ title: 'Your Notification Title',
140
+ body: 'This is the notification message.'
141
+ },
142
+ data: {
143
+ customDataKey: 'customDataValue' // Optional custom data payload
144
+ }
145
+ })
146
+
147
+ assistant.log(`🚩🚩🚩🚩🚩 222 response ${id}`, response); // FLAG
148
+ } catch (e) {
149
+ assistant.error(`🚩🚩🚩🚩🚩 222 error ${id}`, e);
150
+ }
151
+
152
+ try {
153
+ const response = await admin.messaging().sendMulticast({
154
+ tokens: batch,
155
+ notification: note.notification,
156
+ })
157
+
158
+ assistant.log(`🚩🚩🚩🚩🚩 333 response ${id}`, response); // FLAG
159
+ } catch (e) {
160
+ assistant.error(`🚩🚩🚩🚩🚩 333 error ${id}`, e);
161
+ }
162
+
163
+ try {
164
+ const messages = batch.map(token => ({
165
+ token: token,
166
+ notification: {
167
+ title: 'Your Notification Title',
168
+ body: 'This is the notification message.'
169
+ },
170
+ }));
171
+
172
+ const response = await admin.messaging().sendEach(messages);
173
+
174
+ assistant.log(`🚩🚩🚩🚩🚩 444 response ${id}`, response); // FLAG
175
+ } catch (e) {
176
+ assistant.error(`🚩🚩🚩🚩🚩 444 error ${id}`, e);
177
+ }
178
+
179
+ // Send the batch
180
+ const response = await admin.messaging().sendToDevice(batch, note);
181
+
182
+ // Log
183
+ assistant.log(`Sent batch ID: ${id}, Success: ${response.successCount}, Failures: ${response.failureCount}`);
184
+
185
+ // Clean bad tokens
186
+ if (response.failureCount > 0) {
187
+ await self.cleanTokens(batch, response.results, id);
188
+ }
189
+
190
+ // Update response
191
+ payload.response.data.sent += (batch.length - response.failureCount);
192
+ } catch (e) {
193
+ throw assistant.errorify(`Error sending batch ${id}: ${e}`, { code: 500, log: true });
194
+ }
195
+ };
196
+
197
+ Module.prototype.cleanTokens = async function (batch, results, id) {
198
+ const self = this;
199
+
200
+ // Shortcuts
201
+ const Manager = self.Manager;
202
+ const Api = self.Api;
203
+ const assistant = self.assistant;
204
+ const payload = self.payload;
205
+
206
+ // Log
207
+ assistant.log(`Cleaning ${results.length} tokens of batch ID: ${id}`);
208
+
209
+ // Filter out bad tokens
210
+ const cleanPromises = results
211
+ .map((item, index) => {
212
+ // Check if the token is bad
213
+ if (item.error && BAD_TOKEN_REASONS.includes(item.error.code)) {
214
+ const token = batch[index];
215
+
216
+ // Log
217
+ assistant.log(`Found bad token: ${token} (Reason: ${item.error.code})`);
218
+
219
+ // Delete the token
220
+ return self.deleteToken(token, item.error.code);
221
+ }
222
+
223
+ // Return null for valid tokens
224
+ return null;
225
+ })
226
+ // Filter out nulls for valid tokens
227
+ .filter(Boolean);
228
+
229
+ // Clean bad tokens
230
+ try {
231
+ await Promise.all(cleanPromises);
232
+ assistant.log(`Completed cleaning tokens for batch ID: ${id}`);
233
+ } catch (e) {
234
+ assistant.error(`Error cleaning tokens for batch ID: ${id}`, e);
235
+ }
236
+ };
237
+
238
+ Module.prototype.deleteToken = async function (token, errorCode) {
239
+ const self = this;
240
+
241
+ // Shortcuts
242
+ const Manager = self.Manager;
243
+ const Api = self.Api;
244
+ const assistant = self.assistant;
245
+ const payload = self.payload;
246
+
247
+ // Libraries
248
+ const { admin } = self.libraries;
249
+
250
+ // Delete the token
251
+ try {
252
+ // Delete the token
253
+ await admin.firestore().doc(`${PATH_NOTIFICATIONS}/${token}`).delete();
254
+
255
+ // Log
256
+ assistant.log(`Deleted bad token: ${token} (Reason: ${errorCode})`);
257
+
258
+ // Update response
259
+ payload.response.data.deleted++;
260
+ } catch (error) {
261
+ assistant.error(`Failed to delete bad token: ${token} (Reason: ${errorCode}). Error: ${error}`);
262
+ }
263
+ };
264
+
265
+ module.exports = Module;
@@ -0,0 +1,327 @@
1
+ // Constants
2
+ const PATH_NOTIFICATIONS = 'notifications';
3
+ const BAD_TOKEN_REASONS = [
4
+ 'messaging/invalid-registration-token',
5
+ 'messaging/registration-token-not-registered',
6
+ ];
7
+ const BATCH_SIZE = 500;
8
+
9
+ // Module
10
+ function Module() {
11
+
12
+ }
13
+
14
+ // Main
15
+ Module.prototype.main = function () {
16
+ const self = this;
17
+
18
+ // Shortcuts
19
+ const Manager = self.Manager;
20
+ const Api = self.Api;
21
+ const assistant = self.assistant;
22
+ const payload = self.payload;
23
+
24
+ return new Promise(async function(resolve, reject) {
25
+ // Set up response obj
26
+ payload.response.data = {
27
+ subscribers: 0,
28
+ batches: 0,
29
+ sent: 0,
30
+ deleted: 0,
31
+ }
32
+
33
+ // Fix notification payload
34
+ // Yes, it needs to be NESTED!!! DO NOT REMOVE THE NEST!
35
+ const note = {
36
+ notification: {
37
+ title: payload.data.payload.title || 'Notification',
38
+ body: payload.data.payload.body || 'Check this out',
39
+ icon: payload.data.payload.icon || 'https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/socials/itw-creative-works-brandmark-square-black-1024x1024.png',
40
+ click_action: payload.data.payload.clickAction || 'https://itwcreativeworks.com',
41
+ }
42
+ }
43
+
44
+ // Set notification payload
45
+ try {
46
+ const url = new URL(note.notification.click_action);
47
+ url.searchParams.set('cb', new Date().getTime());
48
+ note.notification.click_action = url.toString();
49
+ } catch (e) {
50
+ reject(assistant.errorify(`Failed to add cb to URL: ${e}`, {code: 400, log: true}));
51
+ }
52
+
53
+ // Log
54
+ assistant.log('Resolved notification payload', note)
55
+
56
+ // Check if user is admin
57
+ if (!payload.user.roles.admin) {
58
+ return reject(assistant.errorify(`Admin required.`, {code: 401}));
59
+ }
60
+
61
+ // Check if title and body are set
62
+ if (!note.notification.title || !note.notification.body) {
63
+ return reject(assistant.errorify(`Parameters <title> and <body> required`, {code: 400, sentry: true}));
64
+ }
65
+
66
+ await self.processTokens(note, {tags: false})
67
+ .then(r => {
68
+ return resolve({data: payload.response.data})
69
+ })
70
+ .catch(e => {
71
+ return reject(assistant.errorify(`Failed to send notification: ${e}`, {code: 400, sentry: true}));
72
+ })
73
+ });
74
+
75
+ };
76
+
77
+ // HELPERS //
78
+ Module.prototype.processTokens = async function (note, options) {
79
+ const self = this;
80
+
81
+ // Shortcuts
82
+ const Manager = self.Manager;
83
+ const Api = self.Api;
84
+ const assistant = self.assistant;
85
+ const payload = self.payload;
86
+
87
+ // Set options
88
+ options = options || {};
89
+ options.tags = options.tags || false;
90
+
91
+ // Define collection path
92
+ const queryConditions = options.tags
93
+ ? [{ field: 'tags', operator: 'array-contains-any', value: options.tags }]
94
+ : [];
95
+
96
+ // Batch processing logic
97
+ await Manager.Utilities().iterateCollection(
98
+ async (batch, index) => {
99
+ let batchTokens = [];
100
+
101
+ // Collect tokens from the current batch
102
+ for (const doc of batch.docs) {
103
+ const data = doc.data();
104
+ batchTokens.push(data.token);
105
+ }
106
+
107
+ // Send the batch
108
+ try {
109
+ assistant.log(`Sending batch ${index} with ${batchTokens.length} tokens.`);
110
+ await self.sendBatch(batchTokens, note, index);
111
+ } catch (e) {
112
+ assistant.error(`Error sending batch ${index}`, e);
113
+ }
114
+ },
115
+ {
116
+ collection: PATH_NOTIFICATIONS,
117
+ where: queryConditions,
118
+ batchSize: BATCH_SIZE,
119
+ log: true,
120
+ }
121
+ )
122
+ .then(() => {
123
+ assistant.log('All batches processed successfully.');
124
+ })
125
+ .catch(e => {
126
+ assistant.errorify(`Error during token processing: ${e}`, { code: 500, log: true });
127
+ });
128
+ };
129
+
130
+ // Sending using SDK
131
+ // https://firebase.google.com/docs/cloud-messaging/send-message#send-messages-to-multiple-devices
132
+
133
+ // Manually sending
134
+ // https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send
135
+ // https://firebase.google.com/docs/cloud-messaging/migrate-v1#provide-credentials-manually
136
+ Module.prototype.sendBatch = async function (batch, note, id) {
137
+ const self = this;
138
+
139
+ // Shortcuts
140
+ const Manager = self.Manager;
141
+ const Api = self.Api;
142
+ const assistant = self.assistant;
143
+ const payload = self.payload;
144
+
145
+ // Libraries
146
+ const { admin } = self.libraries;
147
+
148
+ try {
149
+ // Log
150
+ assistant.log(`Sending batch ID: ${id}`);
151
+ console.log('🚩🚩🚩🚩🚩', 1, ); // FLAG
152
+ assistant.log(`batch`, batch);
153
+ assistant.log(`note`, note);
154
+
155
+ try {
156
+ const response = await admin.messaging().sendMulticast({
157
+ tokens: batch,
158
+ // "webpush": {
159
+ // "notification": {
160
+ // "actions": [],
161
+ // "tag": 123456,
162
+ // "title": "Titre de test",
163
+ // "body": "Un contenu de notification",
164
+ // "image": ".\/images\/icon.png",
165
+ // "badge": ".\/images\/badge.png",
166
+ // "icon": ".\/images\/icon256.png",
167
+ // "vibrate": [50, 200, 50],
168
+ // "click_action": "https://somiibo.com",
169
+ // },
170
+ // "data": {
171
+ // "time": "1531396372"
172
+ // },
173
+ // "headers": {
174
+ // "TTL": "60"
175
+ // }
176
+ // },
177
+ // DOES NOT WORK
178
+ // "fcm_options": {
179
+ // "link": "https://dummypage.com"
180
+ // }
181
+ notification: {
182
+ title: 'Your Notification Title',
183
+ body: 'This is the notification message.'
184
+ },
185
+ data: {
186
+ customDataKey: 'customDataValue' // Optional custom data payload
187
+ }
188
+ })
189
+
190
+ assistant.log(`🚩🚩🚩🚩🚩 222 response ${id}`, response); // FLAG
191
+ } catch (e) {
192
+ assistant.error(`🚩🚩🚩🚩🚩 222 error ${id}`, e);
193
+ }
194
+
195
+ try {
196
+ const response = await admin.messaging().sendMulticast({
197
+ tokens: batch,
198
+ notification: note.notification,
199
+ })
200
+
201
+ assistant.log(`🚩🚩🚩🚩🚩 333 response ${id}`, response); // FLAG
202
+ } catch (e) {
203
+ assistant.error(`🚩🚩🚩🚩🚩 333 error ${id}`, e);
204
+ }
205
+
206
+ try {
207
+ const messages = batch.map(token => ({
208
+ token: token,
209
+ notification: {
210
+ title: 'Your Notification Title',
211
+ body: 'This is the notification message.'
212
+ },
213
+ }));
214
+
215
+ const response = await admin.messaging().sendEach(messages);
216
+
217
+ assistant.log(`🚩🚩🚩🚩🚩 444 response ${id}`, response); // FLAG
218
+ } catch (e) {
219
+ assistant.error(`🚩🚩🚩🚩🚩 444 error ${id}`, e);
220
+ }
221
+
222
+ try {
223
+ const message = {
224
+ notification: {
225
+ title: 'Your Notification Title',
226
+ body: 'This is the notification message.'
227
+ },
228
+ data: {
229
+ customDataKey: 'customDataValue' // Optional custom data payload
230
+ },
231
+ token: batch[0],
232
+ }
233
+
234
+ const response = await admin.messaging().send(message);
235
+
236
+ assistant.log(`🚩🚩🚩🚩🚩 555 response ${id}`, response); // FLAG
237
+ } catch (e) {
238
+ assistant.error(`🚩🚩🚩🚩🚩 555 error ${id}`, e);
239
+ }
240
+
241
+ // Send the batch
242
+ const response = await admin.messaging().sendToDevice(batch, note);
243
+
244
+ // Log
245
+ assistant.log(`Sent batch ID: ${id}, Success: ${response.successCount}, Failures: ${response.failureCount}`);
246
+
247
+ // Clean bad tokens
248
+ if (response.failureCount > 0) {
249
+ await self.cleanTokens(batch, response.results, id);
250
+ }
251
+
252
+ // Update response
253
+ payload.response.data.sent += (batch.length - response.failureCount);
254
+ } catch (e) {
255
+ throw assistant.errorify(`Error sending batch ${id}: ${e}`, { code: 500, log: true });
256
+ }
257
+ };
258
+
259
+ Module.prototype.cleanTokens = async function (batch, results, id) {
260
+ const self = this;
261
+
262
+ // Shortcuts
263
+ const Manager = self.Manager;
264
+ const Api = self.Api;
265
+ const assistant = self.assistant;
266
+ const payload = self.payload;
267
+
268
+ // Log
269
+ assistant.log(`Cleaning ${results.length} tokens of batch ID: ${id}`);
270
+
271
+ // Filter out bad tokens
272
+ const cleanPromises = results
273
+ .map((item, index) => {
274
+ // Check if the token is bad
275
+ if (item.error && BAD_TOKEN_REASONS.includes(item.error.code)) {
276
+ const token = batch[index];
277
+
278
+ // Log
279
+ assistant.log(`Found bad token: ${token} (Reason: ${item.error.code})`);
280
+
281
+ // Delete the token
282
+ return self.deleteToken(token, item.error.code);
283
+ }
284
+
285
+ // Return null for valid tokens
286
+ return null;
287
+ })
288
+ // Filter out nulls for valid tokens
289
+ .filter(Boolean);
290
+
291
+ // Clean bad tokens
292
+ try {
293
+ await Promise.all(cleanPromises);
294
+ assistant.log(`Completed cleaning tokens for batch ID: ${id}`);
295
+ } catch (e) {
296
+ assistant.error(`Error cleaning tokens for batch ID: ${id}`, e);
297
+ }
298
+ };
299
+
300
+ Module.prototype.deleteToken = async function (token, errorCode) {
301
+ const self = this;
302
+
303
+ // Shortcuts
304
+ const Manager = self.Manager;
305
+ const Api = self.Api;
306
+ const assistant = self.assistant;
307
+ const payload = self.payload;
308
+
309
+ // Libraries
310
+ const { admin } = self.libraries;
311
+
312
+ // Delete the token
313
+ try {
314
+ // Delete the token
315
+ await admin.firestore().doc(`${PATH_NOTIFICATIONS}/${token}`).delete();
316
+
317
+ // Log
318
+ assistant.log(`Deleted bad token: ${token} (Reason: ${errorCode})`);
319
+
320
+ // Update response
321
+ payload.response.data.deleted++;
322
+ } catch (error) {
323
+ assistant.error(`Failed to delete bad token: ${token} (Reason: ${errorCode}). Error: ${error}`);
324
+ }
325
+ };
326
+
327
+ module.exports = Module;
@@ -30,28 +30,40 @@ Module.prototype.main = function () {
30
30
  deleted: 0,
31
31
  }
32
32
 
33
+ // Prefix payload
34
+ payload.data.payload.notification = payload.data.payload.notification || {};
35
+ payload.data.payload.filters = payload.data.payload.filters || {};
36
+
33
37
  // Fix notification payload
34
- // Yes, it needs to be NESTED!!! DO NOT REMOVE THE NEST!
35
- const note = {
36
- notification: {
37
- title: payload.data.payload.title || 'Notification',
38
- body: payload.data.payload.body || 'Check this out',
39
- icon: payload.data.payload.icon || 'https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/socials/itw-creative-works-brandmark-square-black-1024x1024.png',
40
- click_action: payload.data.payload.clickAction || 'https://itwcreativeworks.com',
41
- }
42
- }
38
+ // https://firebase.google.com/docs/reference/admin/node/firebase-admin.messaging.notification.md#notification_interface
39
+ payload.data.payload.notification.title = payload.data.payload.notification.title
40
+ || 'Notification';
41
+ payload.data.payload.notification.body = payload.data.payload.notification.body
42
+ || 'Check this out';
43
+ payload.data.payload.notification.imageUrl = payload.data.payload.notification.icon
44
+ || 'https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/socials/itw-creative-works-brandmark-square-black-1024x1024.png';
45
+ payload.data.payload.notification.click_action = payload.data.payload.notification.clickAction
46
+ || payload.data.payload.notification.click_action
47
+ || 'https://itwcreativeworks.com';
48
+
49
+ // Fix filters
50
+ // TODO: Implement filters
51
+
52
+ // Set notification payload
53
+ const notification = payload.data.payload.notification;
54
+ const filters = payload.data.payload.filters;
43
55
 
44
56
  // Set notification payload
45
57
  try {
46
- const url = new URL(note.notification.click_action);
58
+ const url = new URL(notification.click_action);
47
59
  url.searchParams.set('cb', new Date().getTime());
48
- note.notification.click_action = url.toString();
60
+ notification.click_action = url.toString();
49
61
  } catch (e) {
50
62
  reject(assistant.errorify(`Failed to add cb to URL: ${e}`, {code: 400, log: true}));
51
63
  }
52
64
 
53
65
  // Log
54
- assistant.log('Resolved notification payload', note)
66
+ assistant.log('Resolved notification payload', notification)
55
67
 
56
68
  // Check if user is admin
57
69
  if (!payload.user.roles.admin) {
@@ -59,11 +71,11 @@ Module.prototype.main = function () {
59
71
  }
60
72
 
61
73
  // Check if title and body are set
62
- if (!note.notification.title || !note.notification.body) {
74
+ if (!notification.title || !notification.body) {
63
75
  return reject(assistant.errorify(`Parameters <title> and <body> required`, {code: 400, sentry: true}));
64
76
  }
65
77
 
66
- await self.processTokens(note, {tags: false})
78
+ await self.processTokens(notification, {tags: false})
67
79
  .then(r => {
68
80
  return resolve({data: payload.response.data})
69
81
  })
@@ -75,7 +87,7 @@ Module.prototype.main = function () {
75
87
  };
76
88
 
77
89
  // HELPERS //
78
- Module.prototype.processTokens = async function (note, options) {
90
+ Module.prototype.processTokens = async function (notification, options) {
79
91
  const self = this;
80
92
 
81
93
  // Shortcuts
@@ -107,7 +119,7 @@ Module.prototype.processTokens = async function (note, options) {
107
119
  // Send the batch
108
120
  try {
109
121
  assistant.log(`Sending batch ${index} with ${batchTokens.length} tokens.`);
110
- await self.sendBatch(batchTokens, note, index);
122
+ await self.sendBatch(batchTokens, index, notification);
111
123
  } catch (e) {
112
124
  assistant.error(`Error sending batch ${index}`, e);
113
125
  }
@@ -133,7 +145,16 @@ Module.prototype.processTokens = async function (note, options) {
133
145
  // Manually sending
134
146
  // https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send
135
147
  // https://firebase.google.com/docs/cloud-messaging/migrate-v1#provide-credentials-manually
136
- Module.prototype.sendBatch = async function (batch, note, id) {
148
+
149
+ // Help
150
+ // https://stackoverflow.com/questions/79408734/send-firebase-cloud-messaging-fcm-notification-with-click-action
151
+ // https://stackoverflow.com/questions/72494678/an-error-occurred-when-trying-to-authenticate-to-the-fcm-servers-on-firebase-c
152
+ // https://stackoverflow.com/questions/72552943/how-can-i-add-firebase-admin-role-to-firebase-project-service-accouts
153
+
154
+ // https://stackoverflow.com/questions/50148266/click-action-attribute-for-web-push-notification-through-fcm
155
+ // https://stackoverflow.com/questions/49177428/http-v1-api-click-action-for-webpush-notification/52764782#52764782
156
+ // https://firebase.google.com/docs/cloud-messaging/js/receive#setting_notification_options_in_the_send_request
157
+ Module.prototype.sendBatch = async function (batch, id, notification) {
137
158
  const self = this;
138
159
 
139
160
  // Shortcuts
@@ -147,64 +168,65 @@ Module.prototype.sendBatch = async function (batch, note, id) {
147
168
 
148
169
  try {
149
170
  // Log
150
- assistant.log(`Sending batch ID: ${id}`);
151
- console.log('🚩🚩🚩🚩🚩', 1, ); // FLAG
152
- assistant.log(`batch`, batch);
153
- assistant.log(`note`, note);
171
+ assistant.log(`Sending batch #${id}: tokens=${batch.length}...`, notification);
154
172
 
155
- try {
156
- const response = await admin.messaging().sendMulticast({
157
- tokens: batch,
173
+ // Prepare messages
174
+ // We have to set click_action because Firebase DOES NOT properly handle it in the service worker, so we HANDLE IT OURSELVES
175
+ const messages = batch.map(token => ({
176
+ token: token,
177
+ notification: {
178
+ title: notification.title,
179
+ body: notification.body,
180
+ imageUrl: notification.imageUrl,
181
+ },
182
+ webpush: {
158
183
  notification: {
159
- title: 'Your Notification Title',
160
- body: 'This is the notification message.'
184
+ title: notification.title,
185
+ body: notification.body,
186
+ icon: notification.imageUrl,
187
+ click_action: notification.click_action,
161
188
  },
162
189
  data: {
163
- customDataKey: 'customDataValue' // Optional custom data payload
164
- }
165
- })
166
-
167
- assistant.log(`🚩🚩🚩🚩🚩 222 response ${id}`, response); // FLAG
168
- } catch (e) {
169
- assistant.error(`🚩🚩🚩🚩🚩 222 error ${id}`, e);
170
- }
171
-
172
- try {
173
- const response = await admin.messaging().sendMulticast({
174
- tokens: batch,
175
- notification: note.notification,
176
- })
177
-
178
- assistant.log(`🚩🚩🚩🚩🚩 333 response ${id}`, response); // FLAG
179
- } catch (e) {
180
- assistant.error(`🚩🚩🚩🚩🚩 333 error ${id}`, e);
181
- }
182
-
183
- try {
184
- const messages = batch.map(token => ({
185
- token: token,
186
- notification: {
187
- title: 'Your Notification Title',
188
- body: 'This is the notification message.'
190
+ click_action: notification.click_action,
189
191
  },
190
- }));
191
-
192
- const response = await admin.messaging().sendEach(messages);
193
-
194
- assistant.log(`🚩🚩🚩🚩🚩 444 response ${id}`, response); // FLAG
195
- } catch (e) {
196
- assistant.error(`🚩🚩🚩🚩🚩 444 error ${id}`, e);
197
- }
192
+ fcm_options: {
193
+ link: notification.click_action,
194
+ }
195
+ },
196
+ data: {
197
+ click_action: notification.click_action,
198
+ },
199
+ }));
198
200
 
199
201
  // Send the batch
200
- const response = await admin.messaging().sendToDevice(batch, note);
202
+ // const response = await admin.messaging().sendToDevice(batch, note);
203
+ const response = await admin.messaging().sendEach(messages);
204
+ // responses: [
205
+ // { success: false, error: [FirebaseMessagingError] },
206
+ // {
207
+ // success: true,
208
+ // messageId: 'projects/promo-server-api/messages/1e91fbc4-2d50-4457-addc-b9add252ae7b'
209
+ // },
210
+ // { success: false, error: [FirebaseMessagingError] }
211
+ // ],
212
+ // successCount: 1,
213
+ // failureCount: 2
214
+
215
+ // Log
216
+ assistant.log(`Sent batch #${id}: tokens=${batch.length}, success=${response.successCount}, failures=${response.failureCount}`, JSON.stringify(response));
201
217
 
202
218
  // Log
203
219
  assistant.log(`Sent batch ID: ${id}, Success: ${response.successCount}, Failures: ${response.failureCount}`);
204
220
 
221
+ // Attach token to response
222
+ response.responses = response.responses.map((item, index) => {
223
+ item.token = batch[index];
224
+ return item;
225
+ });
226
+
205
227
  // Clean bad tokens
206
228
  if (response.failureCount > 0) {
207
- await self.cleanTokens(batch, response.results, id);
229
+ await self.cleanTokens(batch, response.responses, id);
208
230
  }
209
231
 
210
232
  // Update response
@@ -230,18 +252,21 @@ Module.prototype.cleanTokens = async function (batch, results, id) {
230
252
  const cleanPromises = results
231
253
  .map((item, index) => {
232
254
  // Check if the token is bad
233
- if (item.error && BAD_TOKEN_REASONS.includes(item.error.code)) {
234
- const token = batch[index];
255
+ const shouldClean = BAD_TOKEN_REASONS.includes(item?.error?.code)
235
256
 
236
- // Log
237
- assistant.log(`Found bad token: ${token} (Reason: ${item.error.code})`);
257
+ // Log
258
+ assistant.log(`Checking #${index}: success=${item.success}, error=${item?.error?.code || null}, clean=${shouldClean}`, item.error);
259
+ assistant.log(`item.error`, item.error);
260
+ assistant.log(`item?.error?.code`, item?.error?.code);
261
+ assistant.log(`item?.error?.message`, item?.error?.message);
238
262
 
239
- // Delete the token
240
- return self.deleteToken(token, item.error.code);
263
+ // Quit if no error
264
+ if (!item.error || !shouldClean) {
265
+ return null;
241
266
  }
242
267
 
243
- // Return null for valid tokens
244
- return null;
268
+ // Delete the token
269
+ return self.deleteToken(item.token, item.error.code);
245
270
  })
246
271
  // Filter out nulls for valid tokens
247
272
  .filter(Boolean);
@@ -278,7 +303,7 @@ Module.prototype.deleteToken = async function (token, errorCode) {
278
303
  // Update response
279
304
  payload.response.data.deleted++;
280
305
  } catch (error) {
281
- assistant.error(`Failed to delete bad token: ${token} (Reason: ${errorCode}). Error: ${error}`);
306
+ assistant.error(`Failed to delete bad token: ${token} (Reason: ${errorCode})`, error);
282
307
  }
283
308
  };
284
309
 
@@ -19,7 +19,6 @@ Module.prototype.main = function () {
19
19
  payload.data.payload.name = payload.data.payload.name;
20
20
 
21
21
  const DEFAULT = {
22
-
23
22
  spamFilter: {
24
23
  ip: 3,
25
24
  email: 3,
@@ -1,7 +1,8 @@
1
- const decode = require('jwt-decode')
2
- const _ = require('lodash')
3
- const fetch = require('wonderful-fetch')
1
+ // Librairies
2
+ const fetch = require('wonderful-fetch');
3
+ const { jwtDecode } = require('jwt-decode');
4
4
 
5
+ // Module
5
6
  function OAuth2() {
6
7
  const self = this;
7
8
  self.provider = 'discord';
@@ -19,6 +20,8 @@ function OAuth2() {
19
20
 
20
21
  OAuth2.prototype.buildUrl = function (state, url) {
21
22
  const self = this;
23
+
24
+ // Shortcuts
22
25
  const Manager = self.Manager;
23
26
  const assistant = self.assistant;
24
27
 
@@ -34,11 +37,16 @@ OAuth2.prototype.buildUrl = function (state, url) {
34
37
 
35
38
  OAuth2.prototype.verifyIdentity = function (tokenizeResult) {
36
39
  const self = this;
40
+
41
+ // Shortcuts
37
42
  const Manager = self.Manager;
38
43
  const assistant = self.assistant;
39
44
 
40
45
  return new Promise(async function(resolve, reject) {
46
+ // Log
47
+ assistant.log('verifyIdentity(): tokenizeResult', tokenizeResult);
41
48
 
49
+ // Get identity
42
50
  const identityResponse = await fetch('https://discord.com/api/users/@me', {
43
51
  timeout: 60000,
44
52
  response: 'json',
@@ -52,8 +60,10 @@ OAuth2.prototype.verifyIdentity = function (tokenizeResult) {
52
60
  .then(json => json)
53
61
  .catch(e => e)
54
62
 
55
- assistant.log('identityResponse', identityResponse);
63
+ // Log
64
+ assistant.log('verifyIdentity(): identityResponse', identityResponse);
56
65
 
66
+ // Check if error
57
67
  if (identityResponse instanceof Error) {
58
68
  return reject(identityResponse);
59
69
  }
@@ -1,7 +1,8 @@
1
- const decode = require('jwt-decode')
2
- const _ = require('lodash')
3
- const fetch = require('wonderful-fetch')
1
+ // Librairies
2
+ const fetch = require('wonderful-fetch');
3
+ const { jwtDecode } = require('jwt-decode');
4
4
 
5
+ // Module
5
6
  function OAuth2() {
6
7
  const self = this;
7
8
  self.provider = 'google';
@@ -18,6 +19,8 @@ function OAuth2() {
18
19
 
19
20
  OAuth2.prototype.buildUrl = function (state, url) {
20
21
  const self = this;
22
+
23
+ // Shortcuts
21
24
  const Manager = self.Manager;
22
25
  const assistant = self.assistant;
23
26
 
@@ -35,13 +38,20 @@ OAuth2.prototype.buildUrl = function (state, url) {
35
38
 
36
39
  OAuth2.prototype.verifyIdentity = function (tokenizeResult) {
37
40
  const self = this;
41
+
42
+ // Shortcuts
38
43
  const Manager = self.Manager;
39
44
  const assistant = self.assistant;
40
45
 
41
46
  return new Promise(async function(resolve, reject) {
42
- const decoded = decode(tokenizeResult.id_token);
47
+ // Log
48
+ assistant.log('verifyIdentity(): tokenizeResult', tokenizeResult);
49
+
50
+ // Decode token
51
+ const decoded = jwtDecode(tokenizeResult.id_token);
43
52
 
44
- // console.log('---decoded', decoded);
53
+ // Log
54
+ assistant.log('verifyIdentity(): decoded', decoded);
45
55
 
46
56
  // Check if exists
47
57
  Manager.libraries.admin.firestore().collection(`users`)
@@ -1068,14 +1068,14 @@ BackendAssistant.prototype.parseMultipartFormData = function (options) {
1068
1068
  }
1069
1069
 
1070
1070
  const isJWT = (token) => {
1071
- // Ensure the token has three parts separated by dots
1072
- const parts = token.split('.');
1071
+ const { jwtDecode } = require('jwt-decode');
1073
1072
 
1074
1073
  try {
1075
- // Decode the header (first part) to verify it is JSON
1076
- const header = JSON.parse(Buffer.from(parts[0], 'base64').toString('utf8'));
1074
+ // Decode the token and request the header
1075
+ const decoded = jwtDecode(token, { header: true });
1076
+
1077
1077
  // Check for expected JWT keys in the header
1078
- return header.alg && header.typ === 'JWT';
1078
+ return decoded?.alg && decoded?.typ === 'JWT';
1079
1079
  } catch (err) {
1080
1080
  // If parsing fails, it's not a valid JWT
1081
1081
  return false;
@@ -152,6 +152,9 @@ Manager.prototype.init = function (exporter, options) {
152
152
  ? self.assistant.meta.environment
153
153
  : process.env.ENVIRONMENT;
154
154
 
155
+ // Set BEM env variables
156
+ process.env.BEM_FUNCTIONS_URL = self.project.functionsUrl;
157
+
155
158
  // Use the working Firebase logger that they disabled for whatever reason
156
159
  if (
157
160
  process.env.GCLOUD_PROJECT